blob: 5d565952529aeda63d9329886d580952c0c2080a [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/manifest/manifest_parser.h"
#include <stdint.h>
#include <algorithm>
#include <cstddef>
#include <iterator>
#include <optional>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "services/device/public/mojom/screen_orientation_lock_types.mojom-data-view.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/functional/overload.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/safe_url_pattern.h"
#include "third_party/blink/public/mojom/manifest/display_mode.mojom-data-view.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom-blink.h"
#include "third_party/blink/public/mojom/manifest/manifest_launch_handler.mojom-blink.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/text/string_impl.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_uchar.h"
#include "third_party/liburlpattern/part.h"
namespace blink {
using liburlpattern::PartType;
using testing::AllOf;
using testing::Field;
namespace {
bool IsManifestEmpty(const mojom::blink::ManifestPtr& manifest) {
return manifest == mojom::blink::Manifest::New();
}
// Holds values to be compared against a `SafeUrlPattern`. See also the
// `PatternDataEq` matcher.
struct UrlPatternData {
// Represents either a `liburlpattern::Part::value` or a
// `liburlpattern::Part::type`.
using ValueOrType = std::variant<std::string_view, PartType>;
// The fields below correspond to the fields of `SafeUrlPattern`.
std::vector<ValueOrType> protocol;
std::vector<ValueOrType> username;
std::vector<ValueOrType> password;
std::vector<ValueOrType> hostname;
std::vector<ValueOrType> port;
std::vector<ValueOrType> pathname;
std::vector<ValueOrType> search;
std::vector<ValueOrType> hash;
};
// Matcher to compare a `liburlpattern::Part` to a `ValueOrType`.
testing::Matcher<const liburlpattern::Part&> PatternPartEq(
const UrlPatternData::ValueOrType& value_or_type) {
// The docs in `liburlpattern::Part::value` explain the relationship between
// different `part` types and values. Refer to those docs for details.
return std::visit(
absl::Overload(
// When `value_or_type` is a string value, expect `part` has the same
// `value` and its type is `kFixed`.
[](const std::string_view value) {
return AllOf(
Field("type", &liburlpattern::Part::type, PartType::kFixed),
Field("value", &liburlpattern::Part::value, value));
},
// Otherwise, expect `part` has the same `type` and an empty value.
[](const PartType type) {
return AllOf(Field("type", &liburlpattern::Part::type, type),
Field("value", &liburlpattern::Part::value, ""));
}),
value_or_type);
}
// Returns a list of matchers for `values` using `PatternDataEq`.
std::vector<testing::Matcher<const liburlpattern::Part&>> PatternDataMatchers(
const std::vector<UrlPatternData::ValueOrType>& values) {
std::vector<testing::Matcher<const liburlpattern::Part&>> result;
std::ranges::transform(values, std::back_inserter(result), &PatternPartEq);
return result;
}
// Matches a `SafeUrlPattern` by the values and types of its parts against the
// given `expected` data.
testing::Matcher<const SafeUrlPattern&> PatternDataEq(
const UrlPatternData& expected) {
using testing::ElementsAreArray;
return AllOf(Field("protocol", &SafeUrlPattern::protocol,
ElementsAreArray(PatternDataMatchers(expected.protocol))),
Field("username", &SafeUrlPattern::username,
ElementsAreArray(PatternDataMatchers(expected.username))),
Field("password", &SafeUrlPattern::password,
ElementsAreArray(PatternDataMatchers(expected.password))),
Field("hostname", &SafeUrlPattern::hostname,
ElementsAreArray(PatternDataMatchers(expected.hostname))),
Field("port", &SafeUrlPattern::port,
ElementsAreArray(PatternDataMatchers(expected.port))),
Field("pathname", &SafeUrlPattern::pathname,
ElementsAreArray(PatternDataMatchers(expected.pathname))),
Field("search", &SafeUrlPattern::search,
ElementsAreArray(PatternDataMatchers(expected.search))),
Field("hash", &SafeUrlPattern::hash,
ElementsAreArray(PatternDataMatchers(expected.hash))));
}
} // namespace
class ManifestParserTest : public SimTest {
public:
ManifestParserTest(const ManifestParserTest&) = delete;
ManifestParserTest& operator=(const ManifestParserTest&) = delete;
protected:
ManifestParserTest() = default;
~ManifestParserTest() override = default;
mojom::blink::ManifestPtr& ParseManifestWithURLs(const String& data,
const KURL& manifest_url,
const KURL& document_url) {
ManifestParser parser(data, manifest_url, document_url,
GetDocument().GetExecutionContext());
parser.Parse();
Vector<mojom::blink::ManifestErrorPtr> errors;
parser.TakeErrors(&errors);
errors_.clear();
for (auto& error : errors) {
errors_.push_back(std::move(error->message));
}
manifest_ = parser.TakeManifest();
EXPECT_TRUE(manifest_);
return manifest_;
}
mojom::blink::ManifestPtr& ParseManifest(const String& data) {
return ParseManifestWithURLs(data, DefaultManifestUrl(),
DefaultDocumentUrl());
}
const Vector<String>& errors() const { return errors_; }
unsigned int GetErrorCount() const { return errors_.size(); }
static KURL DefaultDocumentUrl() { return KURL("http://foo.com/index.html"); }
static KURL DefaultManifestUrl() {
return KURL("http://foo.com/manifest.json");
}
bool HasDefaultValuesWithUrls(
const mojom::blink::ManifestPtr& manifest,
const KURL& document_url = DefaultDocumentUrl(),
const KURL& manifest_url = DefaultManifestUrl()) {
mojom::blink::ManifestPtr expected_manifest = mojom::blink::Manifest::New();
// A true "default" manifest would have an empty manifest URL. However in
// these tests we don't want to check for that, rather this method is used
// to check that a manifest has all its fields set to "default" values, but
// also have the expected manifest url.
expected_manifest->manifest_url = manifest_url;
expected_manifest->start_url = document_url;
expected_manifest->id = document_url;
expected_manifest->id.RemoveFragmentIdentifier();
expected_manifest->scope = KURL(document_url.BaseAsString().ToString());
return manifest == expected_manifest;
}
private:
mojom::blink::ManifestPtr manifest_;
Vector<String> errors_;
};
TEST_F(ManifestParserTest, CrashTest) {
// Passing temporary variables should not crash.
const String json = R"({"start_url": "/"})";
KURL url("http://example.com");
ManifestParser parser(json, url, url, /*execution_context=*/nullptr);
bool has_comments = parser.Parse();
EXPECT_FALSE(has_comments);
Vector<mojom::blink::ManifestErrorPtr> errors;
parser.TakeErrors(&errors);
auto manifest = parser.TakeManifest();
// .Parse() should have been call without crashing and succeeded.
EXPECT_EQ(0u, errors.size());
EXPECT_FALSE(IsManifestEmpty(manifest));
}
TEST_F(ManifestParserTest, HasComments) {
const String json = R"({
// comment
"start_url": "/"
})";
KURL url("http://example.com");
ManifestParser parser(json, url, url, /*execution_context=*/nullptr);
bool has_comments = parser.Parse();
EXPECT_TRUE(has_comments);
}
TEST_F(ManifestParserTest, EmptyStringNull) {
auto& manifest = ParseManifest("");
// This Manifest is not a valid JSON object, it's a parsing error.
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("Line: 1, column: 1, Syntax error.", errors()[0]);
// A parsing error is equivalent to an empty manifest.
EXPECT_TRUE(IsManifestEmpty(manifest));
EXPECT_FALSE(HasDefaultValuesWithUrls(manifest));
}
TEST_F(ManifestParserTest, ValidNoContentParses) {
base::HistogramTester histogram_tester;
auto& manifest = ParseManifestWithURLs("{}", KURL(), DefaultDocumentUrl());
// Empty Manifest is not a parsing error.
EXPECT_EQ(0u, GetErrorCount());
// Check that the fields are null or set to their default values.
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_TRUE(HasDefaultValuesWithUrls(manifest, DefaultDocumentUrl(), KURL()));
EXPECT_EQ(manifest->dir, mojom::blink::Manifest::TextDirection::kAuto);
EXPECT_TRUE(manifest->name.IsNull());
EXPECT_TRUE(manifest->short_name.IsNull());
EXPECT_EQ(manifest->start_url, DefaultDocumentUrl());
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::DEFAULT);
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_FALSE(manifest->has_background_color);
EXPECT_TRUE(manifest->gcm_sender_id.IsNull());
EXPECT_EQ(DefaultDocumentUrl().BaseAsString(), manifest->scope.GetString());
EXPECT_TRUE(manifest->shortcuts.empty());
// Check that the metrics don't record anything
EXPECT_THAT(histogram_tester.GetAllSamples("Manifest.HasProperty.name"),
testing::IsEmpty());
EXPECT_THAT(histogram_tester.GetAllSamples("Manifest.HasProperty.start_url"),
testing::IsEmpty());
EXPECT_THAT(histogram_tester.GetAllSamples("Manifest.HasProperty.short_name"),
testing::IsEmpty());
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.description"),
testing::IsEmpty());
EXPECT_THAT(histogram_tester.GetAllSamples("Manifest.HasProperty.start_url"),
testing::IsEmpty());
EXPECT_THAT(histogram_tester.GetAllSamples("Manifest.HasProperty.display"),
testing::IsEmpty());
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.orientation"),
testing::IsEmpty());
EXPECT_THAT(histogram_tester.GetAllSamples("Manifest.HasProperty.icons"),
testing::IsEmpty());
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.screenshots"),
testing::IsEmpty());
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.share_target"),
testing::IsEmpty());
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.protocol_handlers"),
testing::IsEmpty());
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.gcm_sender_id"),
testing::IsEmpty());
}
TEST_F(ManifestParserTest, UnrecognizedFieldsIgnored) {
auto& manifest = ParseManifest(
R"({
"unrecognizable_manifest_field": ["foo"],
"name": "bar"
})");
// Unrecognized Manifest fields are not a parsing error.
EXPECT_EQ(0u, GetErrorCount());
// Check that subsequent fields parsed.
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(HasDefaultValuesWithUrls(manifest));
EXPECT_EQ(manifest->name, "bar");
EXPECT_EQ(DefaultDocumentUrl().BaseAsString(), manifest->scope.GetString());
}
TEST_F(ManifestParserTest, MultipleErrorsReporting) {
auto& manifest = ParseManifest(
R"({ "dir": "foo", "name": 42, "short_name": 4,
"id": 12, "orientation": {}, "display": "foo",
"start_url": null, "icons": {}, "theme_color": 42,
"background_color": 42, "shortcuts": {} })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_TRUE(HasDefaultValuesWithUrls(manifest));
EXPECT_THAT(errors(),
testing::UnorderedElementsAre(
"unknown 'dir' value ignored.",
"property 'name' ignored, type string expected.",
"property 'short_name' ignored, type string expected.",
"property 'start_url' ignored, type string expected.",
"property 'id' ignored, type string expected.",
"unknown 'display' value ignored.",
"property 'orientation' ignored, type string expected.",
"property 'icons' ignored, type array expected.",
"property 'theme_color' ignored, type string expected.",
"property 'background_color' ignored, type string expected.",
"property 'shortcuts' ignored, type array expected."));
}
TEST_F(ManifestParserTest, DirParseRules) {
using TextDirection = mojom::blink::Manifest::TextDirection;
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "dir": "ltr" })");
EXPECT_EQ(manifest->dir, TextDirection::kLTR);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(HasDefaultValuesWithUrls(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "dir": " rtl " })");
EXPECT_EQ(manifest->dir, TextDirection::kRTL);
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if dir isn't a string.
{
auto& manifest = ParseManifest(R"({ "dir": {} })");
EXPECT_EQ(manifest->dir, TextDirection::kAuto);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'dir' ignored, type string expected.", errors()[0]);
}
// Don't parse if dir isn't a string.
{
auto& manifest = ParseManifest(R"({ "dir": 42 })");
EXPECT_EQ(manifest->dir, TextDirection::kAuto);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'dir' ignored, type string expected.", errors()[0]);
}
// Accept 'auto'.
{
auto& manifest = ParseManifest(R"({ "dir": "auto" })");
EXPECT_EQ(manifest->dir, TextDirection::kAuto);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'ltr'.
{
auto& manifest = ParseManifest(R"({ "dir": "ltr" })");
EXPECT_EQ(manifest->dir, TextDirection::kLTR);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'rtl'.
{
auto& manifest = ParseManifest(R"({ "dir": "rtl" })");
EXPECT_EQ(manifest->dir, TextDirection::kRTL);
EXPECT_EQ(0u, GetErrorCount());
}
// Parse fails if string isn't known.
{
auto& manifest = ParseManifest(R"({ "dir": "foo" })");
EXPECT_EQ(manifest->dir, TextDirection::kAuto);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("unknown 'dir' value ignored.", errors()[0]);
}
}
TEST_F(ManifestParserTest, NameParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "name": "foo" })");
EXPECT_EQ(manifest->name, "foo");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(HasDefaultValuesWithUrls(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "name": " foo " })");
EXPECT_EQ(manifest->name, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "name": {} })");
EXPECT_TRUE(manifest->name.IsNull());
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' ignored, type string expected.", errors()[0]);
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "name": 42 })");
EXPECT_TRUE(manifest->name.IsNull());
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' ignored, type string expected.", errors()[0]);
}
// Test stripping out of \t \r and \n.
{
auto& manifest = ParseManifest("{ \"name\": \"abc\\t\\r\\ndef\" }");
EXPECT_EQ(manifest->name, "abcdef");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, DescriptionParseRules) {
// Smoke test.
{
auto& manifest =
ParseManifest(R"({ "description": "foo is the new black" })");
EXPECT_EQ(manifest->description, "foo is the new black");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "description": " foo " })");
EXPECT_EQ(manifest->description, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if description isn't a string.
{
auto& manifest = ParseManifest(R"({ "description": {} })");
EXPECT_TRUE(manifest->description.IsNull());
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'description' ignored, type string expected.",
errors()[0]);
}
// Don't parse if description isn't a string.
{
auto& manifest = ParseManifest(R"({ "description": 42 })");
EXPECT_TRUE(manifest->description.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'description' ignored, type string expected.",
errors()[0]);
}
}
TEST_F(ManifestParserTest, ShortNameParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "short_name": "foo" })");
ASSERT_EQ(manifest->short_name, "foo");
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "short_name": " foo " })");
ASSERT_EQ(manifest->short_name, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "short_name": {} })");
ASSERT_TRUE(manifest->short_name.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'short_name' ignored, type string expected.",
errors()[0]);
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "short_name": 42 })");
ASSERT_TRUE(manifest->short_name.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'short_name' ignored, type string expected.",
errors()[0]);
}
// Test stripping out of \t \r and \n.
{
auto& manifest = ParseManifest("{ \"short_name\": \"abc\\t\\r\\ndef\" }");
ASSERT_EQ(manifest->short_name, "abcdef");
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, IdParseRules) {
// Empty manifest.
{
auto& manifest = ParseManifest("{ }");
ASSERT_TRUE(manifest);
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ(manifest->id, DefaultDocumentUrl());
EXPECT_FALSE(manifest->has_custom_id);
}
// Does not contain id field.
{
auto& manifest = ParseManifest(R"({"start_url": "/start?query=a" })");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/start?query=a", manifest->id);
EXPECT_FALSE(manifest->has_custom_id);
}
// Invalid type.
{
auto& manifest =
ParseManifest("{\"start_url\": \"/start?query=a\", \"id\": 1}");
EXPECT_THAT(errors(), testing::ElementsAre(
"property 'id' ignored, type string expected."));
EXPECT_EQ("http://foo.com/start?query=a", manifest->id);
EXPECT_FALSE(manifest->has_custom_id);
}
// Empty string.
{
auto& manifest =
ParseManifest(R"({ "start_url": "/start?query=a", "id": "" })");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/start?query=a", manifest->id);
EXPECT_FALSE(manifest->has_custom_id);
}
// Full url.
{
auto& manifest = ParseManifest(
"{ \"start_url\": \"/start?query=a\", \"id\": \"http://foo.com/foo\" "
"}");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/foo", manifest->id);
EXPECT_TRUE(manifest->has_custom_id);
}
// Full url with different origin.
{
auto& manifest = ParseManifest(
"{ \"start_url\": \"/start?query=a\", \"id\": "
"\"http://another.com/foo\" }");
EXPECT_THAT(
errors(),
testing::ElementsAre(
"property 'id' ignored, should be same origin as document."));
EXPECT_EQ("http://foo.com/start?query=a", manifest->id);
EXPECT_FALSE(manifest->has_custom_id);
}
// Relative path
{
auto& manifest =
ParseManifest("{ \"start_url\": \"/start?query=a\", \"id\": \".\" }");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/", manifest->id);
EXPECT_TRUE(manifest->has_custom_id);
}
// Absolute path
{
auto& manifest =
ParseManifest("{ \"start_url\": \"/start?query=a\", \"id\": \"/\" }");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/", manifest->id);
EXPECT_TRUE(manifest->has_custom_id);
}
// url with fragment
{
auto& manifest = ParseManifest(
"{ \"start_url\": \"/start?query=a\", \"id\": \"/#abc\" }");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/", manifest->id);
EXPECT_TRUE(manifest->has_custom_id);
}
// Smoke test.
{
auto& manifest =
ParseManifest(R"({ "start_url": "/start?query=a", "id": "foo" })");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_EQ("http://foo.com/foo", manifest->id);
EXPECT_TRUE(manifest->has_custom_id);
}
// Invalid UTF-8 character.
{
UChar invalid_utf8_chars[] = {0xD801, 0x0000};
String manifest_str =
String("{ \"start_url\": \"/start?query=a\", \"id\": \"") +
String(invalid_utf8_chars) + String("\" }");
auto& manifest = ParseManifest(manifest_str);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_THAT(
errors()[0].Utf8(),
testing::EndsWith("Unsupported encoding. JSON and all string literals "
"must contain valid Unicode characters."));
ASSERT_TRUE(manifest);
EXPECT_FALSE(manifest->has_custom_id);
}
}
TEST_F(ManifestParserTest, StartURLParseRules) {
// Smoke test.
{
base::HistogramTester histogram_tester;
auto& manifest = ParseManifest(R"({ "start_url": "land.html" })");
ASSERT_EQ(manifest->start_url, KURL(DefaultDocumentUrl(), "land.html"));
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_TRUE(manifest->has_valid_specified_start_url);
EXPECT_FALSE(HasDefaultValuesWithUrls(manifest));
EXPECT_THAT(
histogram_tester.GetAllSamples("Manifest.HasProperty.start_url"),
base::BucketsAre(base::Bucket(1, 1)));
}
// Whitespaces.
{
auto& manifest = ParseManifest(R"({ "start_url": " land.html " })");
ASSERT_EQ(manifest->start_url, KURL(DefaultDocumentUrl(), "land.html"));
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_TRUE(manifest->has_valid_specified_start_url);
}
// Don't parse if property isn't a string.
{
auto& manifest = ParseManifest(R"({ "start_url": {} })");
EXPECT_EQ(manifest->start_url, DefaultDocumentUrl());
EXPECT_EQ(DefaultDocumentUrl(), manifest->id);
EXPECT_THAT(errors(),
testing::ElementsAre(
"property 'start_url' ignored, type string expected."));
EXPECT_FALSE(manifest->has_valid_specified_start_url);
EXPECT_TRUE(HasDefaultValuesWithUrls(manifest));
}
// Don't parse if property isn't a string.
{
auto& manifest = ParseManifest(R"({ "start_url": 42 })");
EXPECT_EQ(manifest->start_url, DefaultDocumentUrl());
EXPECT_THAT(errors(),
testing::ElementsAre(
"property 'start_url' ignored, type string expected."));
EXPECT_FALSE(manifest->has_valid_specified_start_url);
}
// Don't parse if property isn't a valid URL.
{
auto& manifest =
ParseManifest(R"({ "start_url": "http://www.google.ca:a" })");
EXPECT_EQ(manifest->start_url, DefaultDocumentUrl());
EXPECT_THAT(errors(), testing::ElementsAre(
"property 'start_url' ignored, URL is invalid."));
EXPECT_FALSE(manifest->has_valid_specified_start_url);
}
// Absolute start_url, same origin with document.
{
auto& manifest =
ParseManifestWithURLs(R"({ "start_url": "http://foo.com/land.html" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/index.html"));
EXPECT_EQ(manifest->start_url.GetString(), "http://foo.com/land.html");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_TRUE(manifest->has_valid_specified_start_url);
}
// Absolute start_url, cross origin with document.
{
auto& manifest =
ParseManifestWithURLs(R"({ "start_url": "http://bar.com/land.html" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/index.html"));
EXPECT_EQ(manifest->start_url, DefaultDocumentUrl());
EXPECT_THAT(errors(),
testing::ElementsAre("property 'start_url' ignored, should "
"be same origin as document."));
EXPECT_FALSE(manifest->has_valid_specified_start_url);
}
// Resolving has to happen based on the manifest_url.
{
auto& manifest =
ParseManifestWithURLs(R"({ "start_url": "land.html" })",
KURL("http://foo.com/landing/manifest.json"),
KURL("http://foo.com/index.html"));
EXPECT_EQ(manifest->start_url.GetString(),
"http://foo.com/landing/land.html");
EXPECT_THAT(errors(), testing::IsEmpty());
EXPECT_TRUE(manifest->has_valid_specified_start_url);
}
}
TEST_F(ManifestParserTest, ScopeParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "scope": "land", "start_url": "land/landing.html" })");
ASSERT_EQ(manifest->scope, KURL(DefaultDocumentUrl(), "land"));
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_THAT(errors(), testing::IsEmpty());
}
// Whitespaces.
{
auto& manifest = ParseManifest(
R"({ "scope": " land ", "start_url": "land/landing.html" })");
ASSERT_EQ(manifest->scope, KURL(DefaultDocumentUrl(), "land"));
EXPECT_THAT(errors(), testing::IsEmpty());
}
// Return the default value if the property isn't a string.
{
auto& manifest = ParseManifest(R"({ "scope": {} })");
ASSERT_EQ(manifest->scope.GetString(), DefaultDocumentUrl().BaseAsString());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'scope' ignored, type string expected.", errors()[0]);
}
// Return the default value if property isn't a string.
{
auto& manifest = ParseManifest(
R"({ "scope": 42,
"start_url": "http://foo.com/land/landing.html" })");
ASSERT_EQ(manifest->scope, KURL(DefaultDocumentUrl(), "land/"));
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'scope' ignored, type string expected.", errors()[0]);
}
// Absolute scope, start URL is in scope.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "http://foo.com/land",
"start_url": "http://foo.com/land/landing.html" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/index.html"));
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/land");
EXPECT_EQ(0u, GetErrorCount());
}
// Absolute scope, start URL is not in scope.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "http://foo.com/land",
"start_url": "http://foo.com/index.html" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/index.html"));
ASSERT_EQ(manifest->scope.GetString(), DefaultDocumentUrl().BaseAsString());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'scope' ignored. Start url should be within scope "
"of scope URL.",
errors()[0]);
}
// Absolute scope, start URL has different origin than scope URL.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "http://foo.com/land",
"start_url": "http://bar.com/land/landing.html" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/index.html"));
ASSERT_EQ(manifest->scope.GetString(), DefaultDocumentUrl().BaseAsString());
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'start_url' ignored, should be same origin as document.",
errors()[0]);
EXPECT_EQ(
"property 'scope' ignored. Start url should be within scope "
"of scope URL.",
errors()[1]);
}
// scope and start URL have diferent origin than document URL.
{
KURL document_url("http://bar.com/index.html");
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "http://foo.com/land",
"start_url": "http://foo.com/land/landing.html" })",
KURL("http://foo.com/manifest.json"), document_url);
ASSERT_EQ(manifest->scope.GetString(), document_url.BaseAsString());
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'start_url' ignored, should be same origin as document.",
errors()[0]);
EXPECT_EQ(
"property 'scope' ignored. Start url should be within scope "
"of scope URL.",
errors()[1]);
}
// No start URL. Document URL is in a subdirectory of scope.
{
auto& manifest =
ParseManifestWithURLs(R"({ "scope": "http://foo.com/land" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/land/site/index.html"));
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/land");
ASSERT_EQ(0u, GetErrorCount());
}
// No start URL. Document is out of scope.
{
KURL document_url("http://foo.com/index.html");
auto& manifest =
ParseManifestWithURLs(R"({ "scope": "http://foo.com/land" })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/index.html"));
ASSERT_EQ(manifest->scope.GetString(), document_url.BaseAsString());
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'scope' ignored. Start url should be within scope "
"of scope URL.",
errors()[0]);
}
// Resolving has to happen based on the manifest_url.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "treasure" })", KURL("http://foo.com/map/manifest.json"),
KURL("http://foo.com/map/treasure/island/index.html"));
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/map/treasure");
EXPECT_EQ(0u, GetErrorCount());
}
// Scope is parent directory.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": ".." })", KURL("http://foo.com/map/manifest.json"),
KURL("http://foo.com/index.html"));
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/");
EXPECT_EQ(0u, GetErrorCount());
}
// Scope tries to go up past domain.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "../.." })", KURL("http://foo.com/map/manifest.json"),
KURL("http://foo.com/index.html"));
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/");
EXPECT_EQ(0u, GetErrorCount());
}
// Scope removes query args.
{
auto& manifest = ParseManifest(
R"({ "start_url": "app/index.html",
"scope": "/?test=abc" })");
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/");
EXPECT_EQ(0u, GetErrorCount());
}
// Scope removes fragments.
{
auto& manifest = ParseManifest(
R"({ "start_url": "app/index.html",
"scope": "/#abc" })");
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/");
EXPECT_EQ(0u, GetErrorCount());
}
// Scope defaults to start_url with the filename, query, and fragment removed.
{
auto& manifest =
ParseManifest(R"({ "start_url": "land/landing.html?query=test#abc" })");
ASSERT_EQ(manifest->scope, KURL(DefaultDocumentUrl(), "land/"));
EXPECT_EQ(0u, GetErrorCount());
}
{
auto& manifest =
ParseManifest(R"({ "start_url": "land/land/landing.html" })");
ASSERT_EQ(manifest->scope, KURL(DefaultDocumentUrl(), "land/land/"));
EXPECT_EQ(0u, GetErrorCount());
}
// Scope defaults to document_url if start_url is not present.
{
auto& manifest = ParseManifest("{}");
ASSERT_EQ(manifest->scope, KURL(DefaultDocumentUrl(), "."));
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, DisplayParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "display": "browser" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kBrowser);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "display": " browser " })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "display": {} })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'display' ignored,"
" type string expected.",
errors()[0]);
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "display": 42 })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'display' ignored,"
" type string expected.",
errors()[0]);
}
// Parse fails if string isn't known.
{
auto& manifest = ParseManifest(R"({ "display": "browser_something" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("unknown 'display' value ignored.", errors()[0]);
}
// Accept 'fullscreen'.
{
auto& manifest = ParseManifest(R"({ "display": "fullscreen" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kFullscreen);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'standalone'.
{
auto& manifest = ParseManifest(R"({ "display": "standalone" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kStandalone);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'minimal-ui'.
{
auto& manifest = ParseManifest(R"({ "display": "minimal-ui" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kMinimalUi);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'browser'.
{
auto& manifest = ParseManifest(R"({ "display": "browser" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(0u, GetErrorCount());
}
// Case insensitive.
{
auto& manifest = ParseManifest(R"({ "display": "BROWSER" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(0u, GetErrorCount());
}
// Do not accept 'window-controls-overlay' as a display mode.
{
auto& manifest =
ParseManifest(R"({ "display": "window-controls-overlay" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("inapplicable 'display' value ignored.", errors()[0]);
}
// Parsing fails for 'borderless' when Borderless flag is disabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(blink::features::kWebAppBorderless);
auto& manifest = ParseManifest(R"({ "display": "borderless" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("inapplicable 'display' value ignored.", errors()[0]);
}
// Parsing fails for 'borderless' when Borderless flag is enabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(blink::features::kWebAppBorderless);
auto& manifest = ParseManifest(R"({ "display": "borderless" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("inapplicable 'display' value ignored.", errors()[0]);
}
// Parsing fails for 'tabbed' when flag is disabled.
{
ScopedWebAppTabStripForTest tabbed(false);
auto& manifest = ParseManifest(R"({ "display": "tabbed" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("inapplicable 'display' value ignored.", errors()[0]);
}
// Parsing fails for 'tabbed' when flag is enabled.
{
ScopedWebAppTabStripForTest tabbed(true);
auto& manifest = ParseManifest(R"({ "display": "tabbed" })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kUndefined);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("inapplicable 'display' value ignored.", errors()[0]);
}
}
TEST_F(ManifestParserTest, DisplayOverrideParseRules) {
// Smoke test: if no display_override, no value.
{
auto& manifest = ParseManifest(R"({ "display_override": [] })");
EXPECT_TRUE(manifest->display_override.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if not array, value will be ignored
{
auto& manifest = ParseManifest(R"({ "display_override": 23 })");
EXPECT_TRUE(manifest->display_override.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'display_override' ignored, type array expected.",
errors()[0]);
}
// Smoke test: if array value is not a string, it will be ignored
{
auto& manifest = ParseManifest(R"({ "display_override": [ 23 ] })");
EXPECT_TRUE(manifest->display_override.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if array value is not not recognized, it will be ignored
{
auto& manifest = ParseManifest(R"({ "display_override": [ "test" ] })");
EXPECT_TRUE(manifest->display_override.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Case insensitive
{
auto& manifest = ParseManifest(R"({ "display_override": [ "BROWSER" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespace
{
auto& manifest =
ParseManifest(R"({ "display_override": [ " browser " ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'browser'
{
auto& manifest = ParseManifest(R"({ "display_override": [ "browser" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'browser', 'minimal-ui'
{
auto& manifest =
ParseManifest(R"({ "display_override": [ "browser", "minimal-ui" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(manifest->display_override[1],
blink::mojom::DisplayMode::kMinimalUi);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// if array value is not not recognized, it will be ignored
// Accept 'browser', 'minimal-ui'
{
auto& manifest = ParseManifest(
R"({ "display_override": [ 3, "browser", "invalid-display",
"minimal-ui" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(manifest->display_override[1],
blink::mojom::DisplayMode::kMinimalUi);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// validate both display and display-override fields are parsed
// if array value is not not recognized, it will be ignored
// Accept 'browser', 'minimal-ui', 'standalone'
{
auto& manifest = ParseManifest(
R"({ "display": "standalone", "display_override": [ "browser",
"minimal-ui", "standalone" ] })");
EXPECT_EQ(manifest->display, blink::mojom::DisplayMode::kStandalone);
EXPECT_EQ(0u, GetErrorCount());
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(manifest->display_override[1],
blink::mojom::DisplayMode::kMinimalUi);
EXPECT_EQ(manifest->display_override[2],
blink::mojom::DisplayMode::kStandalone);
EXPECT_FALSE(IsManifestEmpty(manifest));
}
// validate duplicate entries.
// Accept 'browser', 'minimal-ui', 'browser'
{
auto& manifest =
ParseManifest(R"({ "display_override": [ "browser", "minimal-ui",
"browser" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBrowser);
EXPECT_EQ(manifest->display_override[1],
blink::mojom::DisplayMode::kMinimalUi);
EXPECT_EQ(manifest->display_override[2],
blink::mojom::DisplayMode::kBrowser);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'window-controls-overlay'.
{
auto& manifest = ParseManifest(
R"({ "display_override": [ "window-controls-overlay" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kWindowControlsOverlay);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Reject 'borderless' when Borderless flag is disabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndDisableFeature(blink::features::kWebAppBorderless);
auto& manifest =
ParseManifest(R"({ "display_override": [ "borderless" ] })");
EXPECT_TRUE(manifest->display_override.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'borderless' when Borderless flag is enabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(blink::features::kWebAppBorderless);
auto& manifest =
ParseManifest(R"({ "display_override": [ "borderless" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kBorderless);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Ignore 'tabbed' when flag is disabled.
{
ScopedWebAppTabStripForTest tabbed(false);
auto& manifest = ParseManifest(R"({ "display_override": [ "tabbed" ] })");
EXPECT_TRUE(manifest->display_override.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'tabbed' when flag is enabled.
{
ScopedWebAppTabStripForTest tabbed(true);
auto& manifest = ParseManifest(R"({ "display_override": [ "tabbed" ] })");
EXPECT_FALSE(manifest->display_override.empty());
EXPECT_EQ(manifest->display_override[0],
blink::mojom::DisplayMode::kTabbed);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, BorderlessUrlPatternsParseRules) {
// Reject 'borderless_url_patterns' when the flag is disabled (default).
{
auto& manifest = ParseManifest(R"({
"borderless_url_patterns": [ {"hostname": "foo.com"} ]
})");
EXPECT_TRUE(manifest->borderless_url_patterns.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'borderless_url_patterns' when the flag is enabled.
{
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(blink::features::kWebAppBorderless);
auto& manifest = ParseManifest(R"({
"borderless_url_patterns": [
{"protocol": "ftp"},
{"hostname": "foo.com"},
{"protocol": "ftp", "hostname": "bar.com"}
]
})");
EXPECT_THAT(
manifest->borderless_url_patterns,
ElementsAre(
PatternDataEq({.protocol = {"ftp"}}),
PatternDataEq({.protocol = {"http"}, .hostname = {"foo.com"}}),
PatternDataEq({.protocol = {"ftp"}, .hostname = {"bar.com"}})));
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, OrientationParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "orientation": "natural" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::NATURAL);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "orientation": "natural" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::NATURAL);
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "orientation": {} })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::DEFAULT);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'orientation' ignored, type string expected.",
errors()[0]);
}
// Don't parse if name isn't a string.
{
auto& manifest = ParseManifest(R"({ "orientation": 42 })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::DEFAULT);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'orientation' ignored, type string expected.",
errors()[0]);
}
// Parse fails if string isn't known.
{
auto& manifest = ParseManifest(R"({ "orientation": "naturalish" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::DEFAULT);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("unknown 'orientation' value ignored.", errors()[0]);
}
// Accept 'any'.
{
auto& manifest = ParseManifest(R"({ "orientation": "any" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::ANY);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'natural'.
{
auto& manifest = ParseManifest(R"({ "orientation": "natural" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::NATURAL);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'landscape'.
{
auto& manifest = ParseManifest(R"({ "orientation": "landscape" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::LANDSCAPE);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'landscape-primary'.
{
auto& manifest = ParseManifest(R"({ "orientation": "landscape-primary" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::LANDSCAPE_PRIMARY);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'landscape-secondary'.
{
auto& manifest =
ParseManifest(R"({ "orientation": "landscape-secondary" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::LANDSCAPE_SECONDARY);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'portrait'.
{
auto& manifest = ParseManifest(R"({ "orientation": "portrait" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::PORTRAIT);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'portrait-primary'.
{
auto& manifest = ParseManifest(R"({ "orientation": "portrait-primary" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::PORTRAIT_PRIMARY);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept 'portrait-secondary'.
{
auto& manifest =
ParseManifest(R"({ "orientation": "portrait-secondary" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::PORTRAIT_SECONDARY);
EXPECT_EQ(0u, GetErrorCount());
}
// Case insensitive.
{
auto& manifest = ParseManifest(R"({ "orientation": "LANDSCAPE" })");
EXPECT_EQ(manifest->orientation,
device::mojom::ScreenOrientationLockType::LANDSCAPE);
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, IconsParseRules) {
// Smoke test: if no icon, no value.
{
auto& manifest = ParseManifest(R"({ "icons": [] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if empty icon, no value.
{
auto& manifest = ParseManifest(R"({ "icons": [ {} ] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: icon with invalid src, no value.
{
auto& manifest = ParseManifest(R"({ "icons": [ { "icons": [] } ] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if icon with empty src, it will be present in the list.
{
auto& manifest = ParseManifest(R"({ "icons": [ { "src": "" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons.size(), 1u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/manifest.json");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if one icons with valid src, it will be present in the list.
{
auto& manifest = ParseManifest(R"({ "icons": [{ "src": "foo.jpg" }] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons.size(), 1u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/foo.jpg");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test.
{
auto& manifest = ParseManifest(R"(
{
"icons": [
{
"src": "foo.webp",
"type": "image/webp",
"sizes": "192x192"
},
{
"src": "foo.svg",
"type": "image/svg+xml",
"sizes": "144x144"
}
]
}
)");
ASSERT_EQ(manifest->icons.size(), 2u);
EXPECT_EQ(manifest->icons[0]->src, KURL(DefaultDocumentUrl(), "foo.webp"));
EXPECT_EQ(manifest->icons[0]->type, "image/webp");
EXPECT_EQ(manifest->icons[0]->sizes.size(), 1u);
EXPECT_EQ(manifest->icons[0]->sizes[0].width(), 192);
EXPECT_EQ(manifest->icons[0]->sizes[0].height(), 192);
EXPECT_EQ(manifest->icons[1]->src, KURL(DefaultDocumentUrl(), "foo.svg"));
EXPECT_EQ(manifest->icons[1]->type, "image/svg+xml");
EXPECT_EQ(manifest->icons[1]->sizes.size(), 1u);
EXPECT_EQ(manifest->icons[1]->sizes[0].width(), 144);
EXPECT_EQ(manifest->icons[1]->sizes[0].height(), 144);
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ScreenshotsParseRules) {
// Smoke test: if no screenshot, no value.
{
auto& manifest = ParseManifest(R"({ "screenshots": [] })");
EXPECT_TRUE(manifest->screenshots.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if empty screenshot, no value.
{
auto& manifest = ParseManifest(R"({ "screenshots": [ {} ] })");
EXPECT_TRUE(manifest->screenshots.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: screenshot with invalid src, no value.
{
auto& manifest =
ParseManifest(R"({ "screenshots": [ { "screenshots": [] } ] })");
EXPECT_TRUE(manifest->screenshots.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if screenshot with empty src, it will be present in the list.
{
auto& manifest = ParseManifest(R"({ "screenshots": [ { "src": "" } ] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->image->src.GetString(),
"http://foo.com/manifest.json");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if one icons has valid src, it will be present in the list.
{
auto& manifest =
ParseManifest(R"({ "screenshots": [{ "src": "foo.jpg" }] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->image->src.GetString(), "http://foo.com/foo.jpg");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ScreenshotFormFactorParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "screenshots": [{ "src": "foo.jpg", "form_factor": "narrow" }] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->form_factor,
mojom::blink::ManifestScreenshot::FormFactor::kNarrow);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Unspecified.
{
auto& manifest =
ParseManifest(R"({ "screenshots": [{ "src": "foo.jpg"}] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->form_factor,
mojom::blink::ManifestScreenshot::FormFactor::kUnknown);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Invalid type.
{
auto& manifest = ParseManifest(
R"({ "screenshots": [{ "src": "foo.jpg", "form_factor": 1}] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->form_factor,
mojom::blink::ManifestScreenshot::FormFactor::kUnknown);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(1u, GetErrorCount());
}
// Unrecognized string.
{
auto& manifest = ParseManifest(
R"({ "screenshots": [{ "src": "foo.jpg", "form_factor": "windows"}] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->form_factor,
mojom::blink::ManifestScreenshot::FormFactor::kUnknown);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(1u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ScreenshotLabelRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "screenshots": [{ "src": "foo.jpg", "label": "example screenshot." }] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->label, "example screenshot.");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Unspecified.
{
auto& manifest =
ParseManifest(R"({ "screenshots": [{ "src": "foo.jpg"}] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_TRUE(screenshots[0]->label.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Empty string.
{
auto& manifest = ParseManifest(
R"({ "screenshots": [{ "src": "foo.jpg", "label": "" }] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_EQ(screenshots[0]->label, "");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Invalid type.
{
auto& manifest = ParseManifest(
R"({ "screenshots": [{ "src": "foo.jpg", "label": 2 }] })");
EXPECT_FALSE(manifest->screenshots.empty());
auto& screenshots = manifest->screenshots;
EXPECT_EQ(screenshots.size(), 1u);
EXPECT_TRUE(screenshots[0]->label.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(1u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, IconSrcParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "foo.png" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->src, KURL(DefaultDocumentUrl(), "foo.png"));
EXPECT_EQ(0u, GetErrorCount());
}
// Whitespaces.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": " foo.png " } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->src, KURL(DefaultDocumentUrl(), "foo.png"));
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if property isn't a string.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": {} } ] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'src' ignored, type string expected.", errors()[0]);
}
// Don't parse if property isn't a string.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": 42 } ] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'src' ignored, type string expected.", errors()[0]);
}
// Resolving has to happen based on the document_url.
{
auto& manifest = ParseManifestWithURLs(
R"({ "icons": [ {"src": "icons/foo.png" } ] })",
KURL("http://foo.com/landing/index.html"), DefaultManifestUrl());
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->src.GetString(),
"http://foo.com/landing/icons/foo.png");
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, IconTypeParseRules) {
// Smoke test.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "type": "foo" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->type, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "type": " foo " } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->type, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if property isn't a string.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "type": {} } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_TRUE(manifest->icons[0]->type.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'type' ignored, type string expected.", errors()[0]);
}
// Don't parse if property isn't a string.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "type": 42 } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_TRUE(manifest->icons[0]->type.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'type' ignored, type string expected.", errors()[0]);
}
}
TEST_F(ManifestParserTest, IconSizesParseRules) {
// Smoke test.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "sizes": "42x42" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 1u);
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "sizes": " 42x42 " } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 1u);
EXPECT_EQ(0u, GetErrorCount());
}
// Ignore sizes if property isn't a string.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "sizes": {} } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 0u);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'sizes' ignored, type string expected.", errors()[0]);
}
// Ignore sizes if property isn't a string.
{
auto& manifest =
ParseManifest(R"({ "icons": [ {"src": "", "sizes": 42 } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 0u);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'sizes' ignored, type string expected.", errors()[0]);
}
// Smoke test: value correctly parsed.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "42x42 48x48" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->sizes[0], gfx::Size(42, 42));
EXPECT_EQ(icons[0]->sizes[1], gfx::Size(48, 48));
EXPECT_EQ(0u, GetErrorCount());
}
// <WIDTH>'x'<HEIGHT> and <WIDTH>'X'<HEIGHT> are equivalent.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "42X42 48X48" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->sizes[0], gfx::Size(42, 42));
EXPECT_EQ(icons[0]->sizes[1], gfx::Size(48, 48));
EXPECT_EQ(0u, GetErrorCount());
}
// Twice the same value is parsed twice.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "42X42 42x42" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->sizes[0], gfx::Size(42, 42));
EXPECT_EQ(icons[0]->sizes[1], gfx::Size(42, 42));
EXPECT_EQ(0u, GetErrorCount());
}
// Width or height can't start with 0.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "004X007 042x00" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 0u);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("found icon with no valid size.", errors()[0]);
}
// Width and height MUST contain digits.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "e4X1.0 55ax1e10" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 0u);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("found icon with no valid size.", errors()[0]);
}
// 'any' is correctly parsed and transformed to gfx::Size(0,0).
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "any AnY ANY aNy" } ] })");
gfx::Size any = gfx::Size(0, 0);
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->sizes.size(), 4u);
EXPECT_EQ(icons[0]->sizes[0], any);
EXPECT_EQ(icons[0]->sizes[1], any);
EXPECT_EQ(icons[0]->sizes[2], any);
EXPECT_EQ(icons[0]->sizes[3], any);
EXPECT_EQ(0u, GetErrorCount());
}
// Some invalid width/height combinations.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "sizes": "x 40xx 1x2x3 x42 42xx42" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->sizes.size(), 0u);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("found icon with no valid size.", errors()[0]);
}
}
TEST_F(ManifestParserTest, IconPurposeParseRules) {
const String kPurposeParseStringError =
"property 'purpose' ignored, type string expected.";
const String kPurposeInvalidValueError =
"found icon with no valid purpose; ignoring it.";
const String kSomeInvalidPurposeError =
"found icon with one or more invalid purposes; those purposes are "
"ignored.";
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": "any" } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->purpose.size(), 1u);
EXPECT_EQ(0u, GetErrorCount());
}
// Trim leading and trailing whitespaces.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": " any " } ] })");
EXPECT_FALSE(manifest->icons.empty());
EXPECT_EQ(manifest->icons[0]->purpose.size(), 1u);
EXPECT_EQ(0u, GetErrorCount());
}
// 'any' is added when property isn't present.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->purpose.size(), 1u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::ANY);
EXPECT_EQ(0u, GetErrorCount());
}
// 'any' is added with error message when property isn't a string (is a
// number).
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": 42 } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->purpose.size(), 1u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::ANY);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(kPurposeParseStringError, errors()[0]);
}
// 'any' is added with error message when property isn't a string (is a
// dictionary).
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": {} } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
EXPECT_EQ(icons[0]->purpose.size(), 1u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::ANY);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(kPurposeParseStringError, errors()[0]);
}
// Smoke test: values correctly parsed.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": "Any Monochrome Maskable" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
ASSERT_EQ(icons[0]->purpose.size(), 3u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::ANY);
EXPECT_EQ(icons[0]->purpose[1],
mojom::blink::ManifestImageResource::Purpose::MONOCHROME);
EXPECT_EQ(icons[0]->purpose[2],
mojom::blink::ManifestImageResource::Purpose::MASKABLE);
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces between values.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": " Any Monochrome " } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
ASSERT_EQ(icons[0]->purpose.size(), 2u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::ANY);
EXPECT_EQ(icons[0]->purpose[1],
mojom::blink::ManifestImageResource::Purpose::MONOCHROME);
EXPECT_EQ(0u, GetErrorCount());
}
// Twice the same value is parsed twice.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": "monochrome monochrome" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
ASSERT_EQ(icons[0]->purpose.size(), 2u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::MONOCHROME);
EXPECT_EQ(icons[0]->purpose[1],
mojom::blink::ManifestImageResource::Purpose::MONOCHROME);
EXPECT_EQ(0u, GetErrorCount());
}
// Invalid icon purpose is ignored.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": "monochrome fizzbuzz" } ] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
ASSERT_EQ(icons[0]->purpose.size(), 1u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::MONOCHROME);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(kSomeInvalidPurposeError, errors()[0]);
}
// If developer-supplied purpose is invalid, entire icon is removed.
{
auto& manifest = ParseManifest(R"({ "icons": [ {"src": "",
"purpose": "fizzbuzz" } ] })");
ASSERT_TRUE(manifest->icons.empty());
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(kPurposeInvalidValueError, errors()[0]);
}
// Two icons, one with an invalid purpose and the other normal.
{
auto& manifest = ParseManifest(
R"({ "icons": [ {"src": "", "purpose": "fizzbuzz" },
{"src": "" }] })");
EXPECT_FALSE(manifest->icons.empty());
auto& icons = manifest->icons;
ASSERT_EQ(1u, icons.size());
ASSERT_EQ(icons[0]->purpose.size(), 1u);
EXPECT_EQ(icons[0]->purpose[0],
mojom::blink::ManifestImageResource::Purpose::ANY);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(kPurposeInvalidValueError, errors()[0]);
}
}
TEST_F(ManifestParserTest, ShortcutsParseRules) {
// Smoke test: if no shortcut, no value.
{
auto& manifest = ParseManifest(R"({ "shortcuts": [] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if empty shortcut, no value.
{
auto& manifest = ParseManifest(R"({ "shortcuts": [ {} ] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[0]);
}
// Smoke test: shortcut with invalid name and url, it will not be present in
// the list.
{
auto& manifest =
ParseManifest(R"({ "shortcuts": [ { "shortcuts": [] } ] })");
EXPECT_TRUE(manifest->icons.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[0]);
}
// Smoke test: shortcut with no name, it will not be present in the list.
{
auto& manifest = ParseManifest(R"({ "shortcuts": [ { "url": "" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' of 'shortcut' not present.", errors()[0]);
}
// Smoke test: shortcut with no url, it will not be present in the list.
{
auto& manifest = ParseManifest(R"({ "shortcuts": [ { "name": "" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[0]);
}
// Smoke test: shortcut with empty name, and empty src, will not be present in
// the list.
{
auto& manifest =
ParseManifest(R"({ "shortcuts": [ { "name": "", "url": "" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' of 'shortcut' is an empty string.", errors()[0]);
}
// Smoke test: shortcut with valid (non-empty) name and src, will be present
// in the list.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [{ "name": "New Post", "url": "compose" }]
})");
EXPECT_FALSE(manifest->shortcuts.empty());
auto& shortcuts = manifest->shortcuts;
EXPECT_EQ(shortcuts.size(), 1u);
EXPECT_EQ(shortcuts[0]->name, "New Post");
EXPECT_EQ(shortcuts[0]->url.GetString(), "http://foo.com/compose");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Validate only the first 10 shortcuts are parsed. The following manifest
// specifies 11 shortcuts, so the last one should not be in the result.
{
auto& manifest = ParseManifest(
R"({
"shortcuts": [
{
"name": "1",
"url": "1"
},
{
"name": "2",
"url": "2"
},
{
"name": "3",
"url": "3"
},
{
"name": "4",
"url": "4"
},
{
"name": "5",
"url": "5"
},
{
"name": "6",
"url": "6"
},
{
"name": "7",
"url": "7"
},
{
"name": "8",
"url": "8"
},
{
"name": "9",
"url": "9"
},
{
"name": "10",
"url": "10"
},
{
"name": "11",
"url": "11"
}
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'shortcuts' contains more than 10 valid elements, "
"only the first 10 are parsed.",
errors()[0]);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
auto& shortcuts = manifest->shortcuts;
EXPECT_EQ(shortcuts.size(), 10u);
EXPECT_EQ(shortcuts[9]->name, "10");
EXPECT_EQ(shortcuts[9]->url.GetString(), "http://foo.com/10");
}
}
TEST_F(ManifestParserTest, ShortcutNameParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "foo", "url": "NameParseTest" } ]
})");
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_EQ(manifest->shortcuts[0]->name, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": " foo ", "url": "NameParseTest"
} ] })");
ASSERT_EQ(manifest->shortcuts[0]->name, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if shortcut->name isn't present.
{
auto& manifest =
ParseManifest(R"({ "shortcuts": [ {"url": "NameParseTest" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' of 'shortcut' not present.", errors()[0]);
}
// Don't parse if shortcut->name isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": {}, "url": "NameParseTest" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' of 'shortcut' ignored, type string expected.",
errors()[0]);
}
// Don't parse if shortcut->name isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": 42, "url": "NameParseTest" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' of 'shortcut' ignored, type string expected.",
errors()[0]);
}
// Don't parse if shortcut->name is an empty string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "", "url": "NameParseTest" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' of 'shortcut' is an empty string.", errors()[0]);
}
}
TEST_F(ManifestParserTest, ShortcutShortNameParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "ShortNameParseTest", "short_name":
"foo", "url": "ShortNameParseTest" } ] })");
ASSERT_EQ(manifest->shortcuts[0]->short_name, "foo");
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Shortcut member is parsed when no short_name is present
{
auto& manifest =
ParseManifest(R"({ "shortcuts": [ {"name": "ShortNameParseTest", "url":
"ShortNameParseTest" } ] })");
ASSERT_TRUE(manifest->shortcuts[0]->short_name.IsNull());
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "ShortNameParseTest", "short_name":
" foo ", "url": "ShortNameParseTest" } ] })");
ASSERT_EQ(manifest->shortcuts[0]->short_name, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse short_name if it isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "ShortNameParseTest", "short_name":
{}, "url": "ShortNameParseTest" } ] })");
ASSERT_TRUE(manifest->shortcuts[0]->short_name.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'short_name' of 'shortcut' ignored, type string expected.",
errors()[0]);
}
// Don't parse short_name if it isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "ShortNameParseTest", "short_name":
42, "url": "ShortNameParseTest" } ] })");
ASSERT_TRUE(manifest->shortcuts[0]->short_name.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'short_name' of 'shortcut' ignored, type string expected.",
errors()[0]);
}
}
TEST_F(ManifestParserTest, ShortcutDescriptionParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {
"name": "DescriptionParseTest",
"description": "foo",
"url": "DescriptionParseTest" } ]
})");
ASSERT_EQ(manifest->shortcuts[0]->description, "foo");
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Shortcut member is parsed when no description is present
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "DescriptionParseTest", "url":
"DescriptionParseTest" } ] })");
ASSERT_TRUE(manifest->shortcuts[0]->description.IsNull());
ASSERT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {
"name": "DescriptionParseTest",
"description": " foo ",
"url": "DescriptionParseTest" } ]
})");
ASSERT_EQ(manifest->shortcuts[0]->description, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse description if it isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {
"name": "DescriptionParseTest",
"description": {},
"url": "DescriptionParseTest" } ]
})");
ASSERT_TRUE(manifest->shortcuts[0]->description.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'description' of 'shortcut' ignored, type string expected.",
errors()[0]);
}
// Don't parse description if it isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {
"name": "DescriptionParseTest",
"description": 42,
"url": "DescriptionParseTest" } ]
})");
ASSERT_TRUE(manifest->shortcuts[0]->description.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'description' of 'shortcut' ignored, type string expected.",
errors()[0]);
}
}
TEST_F(ManifestParserTest, ShortcutUrlParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "UrlParseTest", "url": "foo" } ]
})");
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_EQ(manifest->shortcuts[0]->url, KURL(DefaultDocumentUrl(), "foo"));
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test. Don't parse (with an error) when url is not present.
{
auto& manifest = ParseManifest(R"({ "shortcuts": [ { "name": "" } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[0]);
}
// Whitespaces.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "UrlParseTest", "url": " foo " } ] })");
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_EQ(manifest->shortcuts[0]->url, KURL(DefaultDocumentUrl(), "foo"));
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if url isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "UrlParseTest", "url": {} } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, type string expected.", errors()[0]);
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[1]);
}
// Don't parse if url isn't a string.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "UrlParseTest", "url": 42 } ] })");
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, type string expected.", errors()[0]);
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[1]);
}
// Resolving has to happen based on the manifest_url.
{
auto& manifest = ParseManifestWithURLs(
R"({ "shortcuts": [ {"name": "UrlParseTest", "url": "foo" } ]
})",
KURL("http://foo.com/landing/manifest.json"), DefaultDocumentUrl());
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_EQ(manifest->shortcuts[0]->url.GetString(),
"http://foo.com/landing/foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Shortcut url should have same origin as the document url.
{
auto& manifest = ParseManifestWithURLs(
R"({ "shortcuts": [ {"name": "UrlParseTest", "url":
"http://bar.com/landing" } ]
})",
KURL("http://foo.com/landing/manifest.json"), DefaultDocumentUrl());
EXPECT_TRUE(manifest->shortcuts.empty());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[1]);
}
// Shortcut url should be within the manifest scope.
// The scope will be http://foo.com/landing.
// The shortcut_url will be http://foo.com/shortcut which is in not in scope.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "http://foo.com/landing", "shortcuts": [ {"name":
"UrlParseTest", "url": "shortcut" } ] })",
KURL("http://foo.com/manifest.json"),
KURL("http://foo.com/landing/index.html"));
EXPECT_TRUE(manifest->shortcuts.empty());
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/landing");
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ("property 'url' of 'shortcut' not present.", errors()[1]);
}
// Shortcut url should be within the manifest scope.
// The scope will be http://foo.com/land.
// The shortcut_url will be http://foo.com/land/shortcut which is in scope.
{
auto& manifest = ParseManifestWithURLs(
R"({ "scope": "http://foo.com/land", "start_url":
"http://foo.com/land/landing.html", "shortcuts": [ {"name":
"UrlParseTest", "url": "shortcut" } ] })",
KURL("http://foo.com/land/manifest.json"),
KURL("http://foo.com/index.html"));
EXPECT_FALSE(manifest->shortcuts.empty());
ASSERT_EQ(manifest->scope.GetString(), "http://foo.com/land");
EXPECT_EQ(manifest->shortcuts[0]->url.GetString(),
"http://foo.com/land/shortcut");
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ShortcutIconsParseRules) {
// Smoke test: if no icons, shortcut->icons has no value.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "IconParseTest", "url": "foo",
"icons": [] } ] })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_TRUE(manifest->shortcuts[0]->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if empty icon, shortcut->icons has no value.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "IconParseTest", "url": "foo",
"icons": [{}] } ] })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_TRUE(manifest->shortcuts[0]->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: icon with invalid src, shortcut->icons has no value.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "IconParseTest", "url": "foo",
"icons": [{ "icons": [] }] } ] })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_TRUE(manifest->shortcuts[0]->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if icon with empty src, it will be present in shortcut->icons.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "IconParseTest", "url": "foo",
"icons": [ { "src": "" } ] } ] })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_FALSE(manifest->shortcuts[0]->icons.empty());
auto& icons = manifest->shortcuts[0]->icons;
EXPECT_EQ(icons.size(), 1u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/manifest.json");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if one icon with valid src, it will be present in
// shortcut->icons.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "IconParseTest", "url": "foo",
"icons": [ { "src": "foo.jpg" } ] } ] })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_FALSE(manifest->shortcuts[0]->icons.empty());
auto& icons = manifest->shortcuts[0]->icons;
EXPECT_EQ(icons.size(), 1u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/foo.jpg");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if >1 icon with valid src, it will be present in
// shortcut->icons.
{
auto& manifest = ParseManifest(
R"({ "shortcuts": [ {"name": "IconParseTest", "url": "foo",
"icons": [ {"src": "foo.jpg"}, {"src": "bar.jpg"} ] } ] })");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->shortcuts.empty());
EXPECT_FALSE(manifest->shortcuts[0]->icons.empty());
auto& icons = manifest->shortcuts[0]->icons;
EXPECT_EQ(icons.size(), 2u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/foo.jpg");
EXPECT_EQ(icons[1]->src.GetString(), "http://foo.com/bar.jpg");
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, FileHandlerParseRules) {
base::test::ScopedFeatureList feature_list(
blink::features::kFileHandlingIcons);
// Does not contain file_handlers field.
{
auto& manifest = ParseManifest("{ }");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// file_handlers is not an array.
{
auto& manifest = ParseManifest(R"({ "file_handlers": { } })");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'file_handlers' ignored, type array expected.",
errors()[0]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Contains file_handlers field but no file handlers.
{
auto& manifest = ParseManifest(R"({ "file_handlers": [ ] })");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entries must be objects.
{
auto& manifest = ParseManifest(R"({
"file_handlers": [
"hello world"
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("FileHandler ignored, type object expected.", errors()[0]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry without an action is invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"accept": {
"image/png": [
".png"
]
}
}
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("FileHandler ignored. Property 'action' is invalid.",
errors()[0]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry with an action on a different origin is invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "https://example.com/files",
"accept": {
"image/png": [
".png"
]
}
}
]
})");
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'action' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ("FileHandler ignored. Property 'action' is invalid.",
errors()[1]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry with an action outside of the manifest scope is invalid.
{
auto& manifest = ParseManifest(
R"({
"start_url": "/app/",
"scope": "/app/",
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": [
".png"
]
}
}
]
})");
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'action' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ("FileHandler ignored. Property 'action' is invalid.",
errors()[1]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry without a name is valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": [
".png"
]
}
}
]
})");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(1u, manifest->file_handlers.size());
}
// Entry without an icon is valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"action": "/files",
"accept": {
"image/png": [
".png"
]
}
}
]
})");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(1u, manifest->file_handlers.size());
}
// Entry without an accept is invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files"
}
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[0]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry where accept is not an object is invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": "image/png"
}
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[0]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry where accept extensions are not an array or string is invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": {}
}
}
]
})");
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'accept' type ignored. File extensions must be type array or "
"type string.",
errors()[0]);
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[1]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry where accept extensions are not an array or string is invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": 3
}
}
]
})");
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'accept' type ignored. File extensions must be type array or "
"type string.",
errors()[0]);
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[1]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Entry with an empty list of extensions is not valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": []
}
}
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[0]);
EXPECT_EQ(0u, manifest->file_handlers.size());
}
// Extensions that do not start with a '.' are invalid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": [
"png"
]
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'accept' file extension ignored, must start with a '.'.",
errors()[0]);
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[1]);
ASSERT_EQ(0u, file_handlers.size());
}
// Invalid MIME types and those with parameters are stripped.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "Foo",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image_png": ".png",
"foo/bar": ".foo",
"application/foobar;parameter=25": ".foobar",
"application/its+xml": ".itsml"
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(3u, GetErrorCount());
EXPECT_EQ("invalid MIME type: image_png", errors()[0]);
EXPECT_EQ("invalid MIME type: foo/bar", errors()[1]);
EXPECT_EQ("invalid MIME type: application/foobar;parameter=25",
errors()[2]);
ASSERT_EQ(1u, file_handlers.size());
EXPECT_EQ("Foo", file_handlers[0]->name);
EXPECT_EQ("http://foo.com/foo.jpg",
file_handlers[0]->icons[0]->src.GetString());
EXPECT_EQ(KURL("http://foo.com/files"), file_handlers[0]->action);
ASSERT_EQ(1U, file_handlers[0]->accept.size());
ASSERT_TRUE(file_handlers[0]->accept.Contains("application/its+xml"));
EXPECT_EQ(0u, file_handlers[0]
->accept.find("application/its+xml")
->value.Contains(".foobar"));
}
// Extensions specified as a single string is valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, file_handlers.size());
EXPECT_EQ("name", file_handlers[0]->name);
EXPECT_EQ("http://foo.com/foo.jpg",
file_handlers[0]->icons[0]->src.GetString());
EXPECT_EQ(KURL("http://foo.com/files"), file_handlers[0]->action);
ASSERT_TRUE(file_handlers[0]->accept.Contains("image/png"));
ASSERT_EQ(1u, file_handlers[0]->accept.find("image/png")->value.size());
EXPECT_EQ(".png", file_handlers[0]->accept.find("image/png")->value[0]);
}
// An array of extensions is valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "name",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/jpg": [
".jpg",
".jpeg"
]
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, file_handlers.size());
EXPECT_EQ("name", file_handlers[0]->name);
EXPECT_EQ("http://foo.com/foo.jpg",
file_handlers[0]->icons[0]->src.GetString());
EXPECT_EQ(KURL("http://foo.com/files"), file_handlers[0]->action);
ASSERT_TRUE(file_handlers[0]->accept.Contains("image/jpg"));
ASSERT_EQ(2u, file_handlers[0]->accept.find("image/jpg")->value.size());
EXPECT_EQ(".jpg", file_handlers[0]->accept.find("image/jpg")->value[0]);
EXPECT_EQ(".jpeg", file_handlers[0]->accept.find("image/jpg")->value[1]);
}
// Multiple mime types are valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "Image",
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": ".png",
"image/jpg": [
".jpg",
".jpeg"
]
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, file_handlers.size());
EXPECT_EQ("Image", file_handlers[0]->name);
EXPECT_EQ("http://foo.com/foo.jpg",
file_handlers[0]->icons[0]->src.GetString());
EXPECT_EQ(KURL("http://foo.com/files"), file_handlers[0]->action);
ASSERT_TRUE(file_handlers[0]->accept.Contains("image/jpg"));
ASSERT_EQ(2u, file_handlers[0]->accept.find("image/jpg")->value.size());
EXPECT_EQ(".jpg", file_handlers[0]->accept.find("image/jpg")->value[0]);
EXPECT_EQ(".jpeg", file_handlers[0]->accept.find("image/jpg")->value[1]);
ASSERT_TRUE(file_handlers[0]->accept.Contains("image/png"));
ASSERT_EQ(1u, file_handlers[0]->accept.find("image/png")->value.size());
EXPECT_EQ(".png", file_handlers[0]->accept.find("image/png")->value[0]);
}
// file_handlers with multiple entries is valid.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "Graph",
"icons": [{ "src": "graph.jpg" }],
"action": "/graph",
"accept": {
"text/svg+xml": [
".svg",
".graph"
]
}
},
{
"name": "Raw",
"icons": [{ "src": "raw.jpg" }],
"action": "/raw",
"accept": {
"text/csv": ".csv"
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(2u, file_handlers.size());
EXPECT_EQ("Graph", file_handlers[0]->name);
EXPECT_EQ("http://foo.com/graph.jpg",
file_handlers[0]->icons[0]->src.GetString());
EXPECT_EQ(KURL("http://foo.com/graph"), file_handlers[0]->action);
ASSERT_TRUE(file_handlers[0]->accept.Contains("text/svg+xml"));
ASSERT_EQ(2u, file_handlers[0]->accept.find("text/svg+xml")->value.size());
EXPECT_EQ(".svg", file_handlers[0]->accept.find("text/svg+xml")->value[0]);
EXPECT_EQ(".graph",
file_handlers[0]->accept.find("text/svg+xml")->value[1]);
EXPECT_EQ("Raw", file_handlers[1]->name);
EXPECT_EQ("http://foo.com/raw.jpg",
file_handlers[1]->icons[0]->src.GetString());
EXPECT_EQ(KURL("http://foo.com/raw"), file_handlers[1]->action);
ASSERT_TRUE(file_handlers[1]->accept.Contains("text/csv"));
ASSERT_EQ(1u, file_handlers[1]->accept.find("text/csv")->value.size());
EXPECT_EQ(".csv", file_handlers[1]->accept.find("text/csv")->value[0]);
}
// file_handlers limits the total number of file extensions. Everything after
// and including the file handler that hits the extension limit
{
ManifestParser::SetFileHandlerExtensionLimitForTesting(5);
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"name": "Raw",
"action": "/raw",
"accept": {
"text/csv": ".csv"
}
},
{
"name": "Graph",
"action": "/graph",
"accept": {
"text/svg+xml": [
".graph1",
".graph2",
".graph3",
".graph4",
".graph5",
".graph6"
]
}
},
{
"name": "Data",
"action": "/data",
"accept": {
"text/plain": [
".data"
]
}
}
]
})");
auto& file_handlers = manifest->file_handlers;
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'accept': too many total file extensions, ignoring "
"extensions starting from \".graph5\"",
errors()[0]);
EXPECT_EQ("FileHandler ignored. Property 'accept' is invalid.",
errors()[1]);
ASSERT_EQ(2u, file_handlers.size());
EXPECT_EQ("Raw", file_handlers[0]->name);
EXPECT_EQ(1u, file_handlers[0]->accept.find("text/csv")->value.size());
EXPECT_EQ("Graph", file_handlers[1]->name);
auto accept_map = file_handlers[1]->accept.find("text/svg+xml")->value;
ASSERT_EQ(4u, accept_map.size());
EXPECT_TRUE(accept_map.Contains(".graph1"));
EXPECT_TRUE(accept_map.Contains(".graph2"));
EXPECT_TRUE(accept_map.Contains(".graph3"));
EXPECT_TRUE(accept_map.Contains(".graph4"));
}
// Test `launch_type` parsing and default.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"action": "/files",
"accept": {
"image/png": ".png"
},
"launch_type": "multiple-clients"
},
{
"action": "/files2",
"accept": {
"image/jpeg": ".jpeg"
},
"launch_type": "single-client"
},
{
"action": "/files3",
"accept": {
"text/plain": ".txt"
}
},
{
"action": "/files4",
"accept": {
"text/csv": ".csv"
},
"launch_type": "multiple-client"
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
ASSERT_EQ(4U, manifest->file_handlers.size());
EXPECT_EQ(mojom::blink::ManifestFileHandler::LaunchType::kMultipleClients,
manifest->file_handlers[0]->launch_type);
EXPECT_EQ(mojom::blink::ManifestFileHandler::LaunchType::kSingleClient,
manifest->file_handlers[1]->launch_type);
EXPECT_EQ(mojom::blink::ManifestFileHandler::LaunchType::kSingleClient,
manifest->file_handlers[2]->launch_type);
// This one has a typo.
EXPECT_EQ(mojom::blink::ManifestFileHandler::LaunchType::kSingleClient,
manifest->file_handlers[3]->launch_type);
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("launch_type value 'multiple-client' ignored, unknown value.",
errors()[0]);
}
}
TEST_F(ManifestParserTest, FileHandlerIconsParseRules) {
// Smoke test: if no icons, file_handler->icon has no value.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
EXPECT_TRUE(manifest->file_handlers[0]->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if empty icon, file_handler->icons has no value.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [{}],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
EXPECT_TRUE(manifest->file_handlers[0]->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: icon with invalid src, file_handler->icons has no value.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [{ "icons": [] }],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
EXPECT_TRUE(manifest->file_handlers[0]->icons.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if icon with empty src, it will be present in
// file_handler->icons.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [{ "src": "" }],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
EXPECT_FALSE(manifest->file_handlers[0]->icons.empty());
auto& icons = manifest->file_handlers[0]->icons;
EXPECT_EQ(icons.size(), 1u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/manifest.json");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if one icon with valid src, it will be present in
// file_handler->icons.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [{ "src": "foo.jpg" }],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
EXPECT_FALSE(manifest->file_handlers[0]->icons.empty());
auto& icons = manifest->file_handlers[0]->icons;
EXPECT_EQ(icons.size(), 1u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/foo.jpg");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if >1 icon with valid src, it will be present in
// file_handler->icons.
{
auto& manifest = ParseManifest(
R"({
"file_handlers": [
{
"icons": [{ "src": "foo.jpg" }, { "src": "bar.jpg" }],
"action": "/files",
"accept": {
"image/png": ".png"
}
}
]
})");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_FALSE(manifest->file_handlers.empty());
EXPECT_FALSE(manifest->file_handlers[0]->icons.empty());
auto& icons = manifest->file_handlers[0]->icons;
EXPECT_EQ(icons.size(), 2u);
EXPECT_EQ(icons[0]->src.GetString(), "http://foo.com/foo.jpg");
EXPECT_EQ(icons[1]->src.GetString(), "http://foo.com/bar.jpg");
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ProtocolHandlerParseRules) {
// Does not contain protocol_handlers field.
{
auto& manifest = ParseManifest("{ }");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(0u, manifest->protocol_handlers.size());
}
// protocol_handlers is not an array.
{
auto& manifest = ParseManifest(R"({ "protocol_handlers": { } })");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'protocol_handlers' ignored, type array expected.",
errors()[0]);
EXPECT_EQ(0u, manifest->protocol_handlers.size());
}
// Contains protocol_handlers field but no protocol handlers.
{
auto& manifest = ParseManifest(R"({ "protocol_handlers": [ ] })");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(0u, manifest->protocol_handlers.size());
}
// Entries must be objects
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
"hello world"
]
})");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("protocol_handlers entry ignored, type object expected.",
errors()[0]);
EXPECT_EQ(0u, manifest->protocol_handlers.size());
}
// A valid protocol handler.
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
{
"protocol": "web+github",
"url": "http://foo.com/?profile=%s"
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, protocol_handlers.size());
ASSERT_EQ("web+github", protocol_handlers[0]->protocol);
ASSERT_EQ("http://foo.com/?profile=%s", protocol_handlers[0]->url);
}
// An invalid protocol handler with the URL not being from the same origin.
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
{
"protocol": "web+github",
"url": "http://bar.com/?profile=%s"
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ(
"protocol_handlers entry ignored, required property 'url' is invalid.",
errors()[1]);
ASSERT_EQ(0u, protocol_handlers.size());
}
// An invalid protocol handler with the URL not being within manifest scope.
{
auto& manifest = ParseManifest(
R"({
"start_url": "/app/",
"scope": "/app/",
"protocol_handlers": [
{
"protocol": "web+github",
"url": "/?profile=%s"
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ(
"protocol_handlers entry ignored, required property 'url' is invalid.",
errors()[1]);
ASSERT_EQ(0u, protocol_handlers.size());
}
// An invalid protocol handler with no value for protocol.
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
{
"url": "http://foo.com/?profile=%s"
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"protocol_handlers entry ignored, required property 'protocol' is "
"missing.",
errors()[0]);
ASSERT_EQ(0u, protocol_handlers.size());
}
// An invalid protocol handler with no url.
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
{
"protocol": "web+github"
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"protocol_handlers entry ignored, required property 'url' is missing.",
errors()[0]);
ASSERT_EQ(0u, protocol_handlers.size());
}
// An invalid protocol handler with a url that doesn't contain the %s token.
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
{
"protocol": "web+github",
"url": "http://foo.com/?profile="
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"The url provided ('http://foo.com/?profile=') does not contain '%s'.",
errors()[0]);
EXPECT_EQ(
"protocol_handlers entry ignored, required property 'url' is invalid.",
errors()[1]);
ASSERT_EQ(0u, protocol_handlers.size());
}
// An invalid protocol handler with a non-allowed protocol.
{
auto& manifest = ParseManifest(R"({
"protocol_handlers": [
{
"protocol": "github",
"url": "http://foo.com/?profile="
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"The scheme 'github' doesn't belong to the scheme allowlist. Please "
"prefix non-allowlisted schemes with the string 'web+'.",
errors()[0]);
EXPECT_EQ(
"protocol_handlers entry ignored, required property 'protocol' is "
"invalid.",
errors()[1]);
ASSERT_EQ(0u, protocol_handlers.size());
}
// Multiple valid protocol handlers
{
auto& manifest = ParseManifest(
R"({
"protocol_handlers": [
{
"protocol": "web+github",
"url": "http://foo.com/?profile=%s"
},
{
"protocol": "web+test",
"url": "http://foo.com/?test=%s"
},
{
"protocol": "web+relative",
"url": "relativeURL=%s"
}
]
})");
auto& protocol_handlers = manifest->protocol_handlers;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(3u, protocol_handlers.size());
ASSERT_EQ("web+github", protocol_handlers[0]->protocol);
ASSERT_EQ("http://foo.com/?profile=%s", protocol_handlers[0]->url);
ASSERT_EQ("web+test", protocol_handlers[1]->protocol);
ASSERT_EQ("http://foo.com/?test=%s", protocol_handlers[1]->url);
ASSERT_EQ("web+relative", protocol_handlers[2]->protocol);
ASSERT_EQ("http://foo.com/relativeURL=%s", protocol_handlers[2]->url);
}
}
TEST_F(ManifestParserTest, ScopeExtensionsParseRules) {
// Manifest does not contain a 'scope_extensions' field.
{
auto& manifest = ParseManifest("{ }");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(0u, manifest->scope_extensions.size());
}
// 'scope_extensions' is not an array.
{
auto& manifest = ParseManifest(R"({ "scope_extensions": { } })");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'scope_extensions' ignored, type array expected.",
errors()[0]);
EXPECT_EQ(0u, manifest->scope_extensions.size());
}
// Contains 'scope_extensions' field but no scope extension entries.
{
auto& manifest = ParseManifest(R"({ "scope_extensions": [ ] })");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_EQ(0u, manifest->scope_extensions.size());
}
// Scope extension entry must be an object or a string.
{
auto& manifest = ParseManifest(R"({ "scope_extensions": [ 7 ] })");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("scope_extensions entry ignored, type object expected.",
errors()[0]);
EXPECT_EQ(0u, scope_extensions.size());
}
// A valid scope extension.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type" : "origin", "origin" : "https://foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
}
// Origin field is missing from the scope extension entry.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "invalid_field", "origin": "https://foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("Scope extension 'type' invalid.", errors()[0]);
EXPECT_EQ(0u, scope_extensions.size());
}
// Scope extension missing `type` key
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"origin": "https://foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"scope_extensions entry ignored, required properties 'type' and "
"'origin' "
"are missing.",
errors()[0]);
EXPECT_EQ(0u, scope_extensions.size());
}
// Scope extension missing `origin` key
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "asdf": "http://foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"scope_extensions entry ignored, required properties 'type' and "
"'origin' "
"are missing.",
errors()[0]);
EXPECT_EQ(0u, scope_extensions.size());
}
// Scope extension using unsupported `type` key
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "site", "origin": "http://foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("Scope extension 'type' invalid.", errors()[0]);
EXPECT_EQ(0u, scope_extensions.size());
}
// Scope extension entry origin must be a string.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": 7
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'origin' ignored, type string expected.", errors()[0]);
EXPECT_EQ(0u, scope_extensions.size());
}
// Scheme must be https.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "http://foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"scope_extensions entry ignored, required property 'origin' must use "
"the https scheme.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Origin must be valid.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https:///////"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"scope_extensions entry ignored, required property 'origin' is "
"invalid.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse multiple valid scope extensions.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://foo.com"
},
{
"type": "origin", "origin": "https://bar.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(2u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://bar.com")
->IsSameOriginWith(scope_extensions[1]->origin.get()));
}
// Parse invalid scope extensions list with an array entry.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://foo.com"
},
[]
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("scope_extensions entry ignored, type object expected.",
errors()[0]);
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
}
// Parse shorthand notation as an invalid format
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
"https://bar.com"
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("scope_extensions entry ignored, type object expected.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse both valid and invalid scope extensions.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://foo.com"
},
{
"type": "origin", "origin": "about:"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"scope_extensions entry ignored, required property 'origin' is "
"invalid.",
errors()[0]);
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
}
// Parse invalid scope extension where the origin is a TLD.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://co.uk"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"scope_extensions entry ignored, domain of required property 'origin' "
"is invalid.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse valid IP address as origin.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://192.168.0.1:8888"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(
blink::SecurityOrigin::CreateFromString("https://192.168.0.1:8888")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
ASSERT_FALSE(scope_extensions[0]->has_origin_wildcard);
}
// Validate only the first 10 scope extensions are parsed. The following
// manifest specifies 11 scope extensions, so the last one should not be in
// the result.
{
auto& manifest = ParseManifest(
R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://192.168.0.1:8001"
},
{
"type": "origin", "origin": "https://192.168.0.1:8002"
},
{
"type": "origin", "origin": "https://192.168.0.1:8003"
},
{
"type": "origin", "origin": "https://192.168.0.1:8004"
},
{
"type": "origin", "origin": "https://192.168.0.1:8005"
},
{
"type": "origin", "origin": "https://192.168.0.1:8006"
},
{
"type": "origin", "origin": "https://192.168.0.1:8007"
},
{
"type": "origin", "origin": "https://192.168.0.1:8008"
},
{
"type": "origin", "origin": "https://192.168.0.1:8009"
},
{
"type": "origin", "origin": "https://192.168.0.1:8010"
},
{
"type": "origin", "origin": "https://192.168.0.1:8011"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'scope_extensions' contains more than 10 valid elements, "
"only the first 10 are parsed.",
errors()[0]);
ASSERT_EQ(10u, scope_extensions.size());
ASSERT_TRUE(
blink::SecurityOrigin::CreateFromString("https://192.168.0.1:8010")
->IsSameOriginWith(scope_extensions[9]->origin.get()));
}
}
TEST_F(ManifestParserTest, ScopeExtensionsBySiteParseRules) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(
blink::features::kWebAppEnableScopeExtensionsBySite);
// Parse origin with wildcard.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*.foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
ASSERT_TRUE(scope_extensions[0]->has_origin_wildcard);
}
// Parse origin with wildcard with feature disabled.
{
base::test::ScopedFeatureList inner_feature_list;
inner_feature_list.InitAndDisableFeature(
blink::features::kWebAppEnableScopeExtensionsBySite);
// Valid wildcard format.
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*.foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://*.foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
ASSERT_FALSE(scope_extensions[0]->has_origin_wildcard);
}
// Parse invalid origin wildcard format.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*foo.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://*foo.com")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
ASSERT_FALSE(scope_extensions[0]->has_origin_wildcard);
}
// Parse origin where the host is just the wildcard prefix.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*."
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
ASSERT_EQ(
"scope_extensions entry ignored, domain of required property 'origin' "
"is invalid.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse invalid origin where wildcard is used with a TLD.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*.com"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
ASSERT_EQ(
"scope_extensions entry ignored, domain of required property 'origin' "
"is invalid.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse invalid origin where wildcard is used with an unknown TLD.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*.foo"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
ASSERT_EQ(
"scope_extensions entry ignored, domain of required property 'origin' "
"is invalid.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse invalid origin where wildcard is used with a multipart TLD.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*.co.uk"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(1u, GetErrorCount());
ASSERT_EQ(
"scope_extensions entry ignored, domain of required property 'origin' "
"is invalid.",
errors()[0]);
ASSERT_EQ(0u, scope_extensions.size());
}
// Parse valid origin with private registry.
{
auto& manifest = ParseManifest(R"({
"scope_extensions": [
{
"type": "origin", "origin": "https://*.glitch.me"
}
]
})");
auto& scope_extensions = manifest->scope_extensions;
ASSERT_EQ(0u, GetErrorCount());
ASSERT_EQ(1u, scope_extensions.size());
ASSERT_TRUE(blink::SecurityOrigin::CreateFromString("https://glitch.me")
->IsSameOriginWith(scope_extensions[0]->origin.get()));
ASSERT_TRUE(scope_extensions[0]->has_origin_wildcard);
}
}
TEST_F(ManifestParserTest, LockScreenParseRules) {
KURL manifest_url = KURL("https://foo.com/manifest.json");
KURL document_url = KURL("https://foo.com/index.html");
{
// Manifest does not contain a 'lock_screen' field.
auto& manifest = ParseManifest("{ }");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_TRUE(manifest->lock_screen.is_null());
}
{
// 'lock_screen' is not an object.
auto& manifest = ParseManifest(R"( { "lock_screen": [ ] } )");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'lock_screen' ignored, type object expected.",
errors()[0]);
EXPECT_TRUE(manifest->lock_screen.is_null());
}
{
// Contains 'lock_screen' field but no start_url entry.
auto& manifest = ParseManifest(R"( { "lock_screen": { } } )");
ASSERT_EQ(0u, GetErrorCount());
ASSERT_FALSE(manifest->lock_screen.is_null());
EXPECT_TRUE(manifest->lock_screen->start_url.IsEmpty());
}
{
// 'start_url' entries must be valid URLs.
auto& manifest =
ParseManifest(R"({ "lock_screen": { "start_url": {} } } )");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'start_url' ignored, type string expected.",
errors()[0]);
ASSERT_FALSE(manifest->lock_screen.is_null());
EXPECT_TRUE(manifest->lock_screen->start_url.IsEmpty());
}
{
// 'start_url' entries must be within scope.
auto& manifest = ParseManifest(
R"({ "lock_screen": { "start_url": "https://bar.com" } } )");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'start_url' ignored, should be within scope of the manifest.",
errors()[0]);
ASSERT_FALSE(manifest->lock_screen.is_null());
EXPECT_TRUE(manifest->lock_screen->start_url.IsEmpty());
}
{
// A valid lock_screen start_url entry.
auto& manifest = ParseManifestWithURLs(
R"({
"lock_screen": {
"start_url": "https://foo.com"
}
})",
manifest_url, document_url);
ASSERT_EQ(0u, GetErrorCount());
ASSERT_FALSE(manifest->lock_screen.is_null());
EXPECT_EQ("https://foo.com/", manifest->lock_screen->start_url.GetString());
}
{
// A valid lock_screen start_url entry, parsed relative to manifest URL.
auto& manifest = ParseManifestWithURLs(
R"({
"lock_screen": {
"start_url": "new_note"
}
})",
manifest_url, document_url);
ASSERT_EQ(0u, GetErrorCount());
ASSERT_FALSE(manifest->lock_screen.is_null());
EXPECT_EQ("https://foo.com/new_note",
manifest->lock_screen->start_url.GetString());
}
}
TEST_F(ManifestParserTest, NoteTakingParseRules) {
KURL manifest_url = KURL("https://foo.com/manifest.json");
KURL document_url = KURL("https://foo.com/index.html");
{
// Manifest does not contain a 'note_taking' field.
auto& manifest = ParseManifest("{ }");
ASSERT_EQ(0u, GetErrorCount());
EXPECT_TRUE(manifest->note_taking.is_null());
}
{
// 'note_taking' is not an object.
auto& manifest = ParseManifest(R"( { "note_taking": [ ] } )");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'note_taking' ignored, type object expected.",
errors()[0]);
EXPECT_TRUE(manifest->note_taking.is_null());
}
{
// Contains 'note_taking' field but no new_note_url entry.
auto& manifest = ParseManifest(R"( { "note_taking": { } } )");
ASSERT_EQ(0u, GetErrorCount());
ASSERT_FALSE(manifest->note_taking.is_null());
EXPECT_TRUE(manifest->note_taking->new_note_url.IsEmpty());
}
{
// 'new_note_url' entries must be valid URLs.
auto& manifest =
ParseManifest(R"({ "note_taking": { "new_note_url": {} } } )");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'new_note_url' ignored, type string expected.",
errors()[0]);
ASSERT_FALSE(manifest->note_taking.is_null());
EXPECT_TRUE(manifest->note_taking->new_note_url.IsEmpty());
}
{
// 'new_note_url' entries must be within scope.
auto& manifest = ParseManifest(
R"({ "note_taking": { "new_note_url": "https://bar.com" } } )");
ASSERT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'new_note_url' ignored, should be within scope of the "
"manifest.",
errors()[0]);
ASSERT_FALSE(manifest->note_taking.is_null());
EXPECT_TRUE(manifest->note_taking->new_note_url.IsEmpty());
}
{
// A valid note_taking new_note_url entry.
auto& manifest = ParseManifestWithURLs(
R"({
"note_taking": {
"new_note_url": "https://foo.com"
}
})",
manifest_url, document_url);
ASSERT_EQ(0u, GetErrorCount());
ASSERT_FALSE(manifest->note_taking.is_null());
EXPECT_EQ("https://foo.com/",
manifest->note_taking->new_note_url.GetString());
}
{
// A valid note_taking new_note_url entry, parsed relative to manifest URL.
auto& manifest = ParseManifestWithURLs(
R"({
"note_taking": {
"new_note_url": "new_note"
}
})",
manifest_url, document_url);
ASSERT_EQ(0u, GetErrorCount());
ASSERT_FALSE(manifest->note_taking.is_null());
EXPECT_EQ("https://foo.com/new_note",
manifest->note_taking->new_note_url.GetString());
}
}
TEST_F(ManifestParserTest, ShareTargetParseRules) {
// Contains share_target field but no keys.
{
auto& manifest = ParseManifest(R"({ "share_target": {} })");
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'share_target' ignored. Property 'action' is invalid.",
errors()[0]);
}
// Contains share_target field but no params key.
{
auto& manifest = ParseManifest(R"({ "share_target": { "action": "" } })");
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
EXPECT_EQ(
"property 'share_target' ignored. Property 'params' type "
"dictionary expected.",
errors()[2]);
}
// Contains share_target field but no action key.
{
auto& manifest = ParseManifest(R"({ "share_target": { "params": {} } })");
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'share_target' ignored. Property 'action' is invalid.",
errors()[0]);
}
// Key in share_target that isn't valid.
{
auto& manifest = ParseManifest(
R"({ "share_target": {"incorrect_key": "some_value" } })");
ASSERT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'share_target' ignored. Property 'action' is invalid.",
errors()[0]);
}
}
TEST_F(ManifestParserTest, ShareTargetUrlTemplateParseRules) {
KURL manifest_url = KURL("https://foo.com/manifest.json");
KURL document_url = KURL("https://foo.com/index.html");
// Contains share_target, but action is empty.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "", "params": {} } })", manifest_url,
document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action, manifest_url);
EXPECT_TRUE(manifest->share_target->params->text.IsNull());
EXPECT_TRUE(manifest->share_target->params->title.IsNull());
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
}
// Parse but throw an error if url_template property isn't a string.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "", "params": {} } })", manifest_url,
document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action, manifest_url);
EXPECT_TRUE(manifest->share_target->params->text.IsNull());
EXPECT_TRUE(manifest->share_target->params->title.IsNull());
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
}
// Don't parse if action property isn't a string.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": {}, "params": {} } })", manifest_url,
document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'action' ignored, type string expected.", errors()[0]);
EXPECT_EQ("property 'share_target' ignored. Property 'action' is invalid.",
errors()[1]);
}
// Don't parse if action property isn't a string.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": 42, "params": {} } })", manifest_url,
document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'action' ignored, type string expected.", errors()[0]);
EXPECT_EQ("property 'share_target' ignored. Property 'action' is invalid.",
errors()[1]);
}
// Don't parse if params property isn't a dict.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "", "params": "" } })", manifest_url,
document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
EXPECT_EQ(
"property 'share_target' ignored. Property 'params' type "
"dictionary expected.",
errors()[2]);
}
// Don't parse if params property isn't a dict.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "", "params": 42 } })", manifest_url,
document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
EXPECT_EQ(
"property 'share_target' ignored. Property 'params' type "
"dictionary expected.",
errors()[2]);
}
// Ignore params keys with invalid types.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "", "params": { "text": 42 }
} })",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action, manifest_url);
EXPECT_TRUE(manifest->share_target->params->text.IsNull());
EXPECT_TRUE(manifest->share_target->params->title.IsNull());
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
EXPECT_EQ("property 'text' ignored, type string expected.", errors()[2]);
}
// Ignore params keys with invalid types.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "",
"params": { "title": 42 } } })",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action, manifest_url);
EXPECT_TRUE(manifest->share_target->params->text.IsNull());
EXPECT_TRUE(manifest->share_target->params->title.IsNull());
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
EXPECT_EQ("property 'title' ignored, type string expected.", errors()[2]);
}
// Don't parse if params property has keys with invalid types.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "", "params": { "url": {},
"text": "hi" } } })",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action, manifest_url);
EXPECT_EQ(manifest->share_target->params->text, "hi");
EXPECT_TRUE(manifest->share_target->params->title.IsNull());
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
EXPECT_EQ("property 'url' ignored, type string expected.", errors()[2]);
}
// Don't parse if action property isn't a valid URL.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com:a", "params":
{} } })",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'action' ignored, URL is invalid.", errors()[0]);
EXPECT_EQ("property 'share_target' ignored. Property 'action' is invalid.",
errors()[1]);
}
// Fail parsing if action is at a different origin than the Web
// manifest.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo2.com/",
"params": {} } })",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'action' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ(
"property 'share_target' ignored. Property 'action' is "
"invalid.",
errors()[1]);
}
// Fail parsing if action is not within scope of the manifest.
{
auto& manifest = ParseManifestWithURLs(
R"({ "start_url": "/app/",
"scope": "/app/",
"share_target": { "action": "/",
"params": {} } })",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"property 'action' ignored, should be within scope of the manifest.",
errors()[0]);
EXPECT_EQ(
"property 'share_target' ignored. Property 'action' is "
"invalid.",
errors()[1]);
}
// Smoke test: Contains share_target and action, and action is valid.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": {"action": "share/", "params": {} } })",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action.GetString(),
"https://foo.com/share/");
EXPECT_TRUE(manifest->share_target->params->text.IsNull());
EXPECT_TRUE(manifest->share_target->params->title.IsNull());
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
}
// Smoke test: Contains share_target and action, and action is valid, params
// is populated.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": {"action": "share/", "params": { "text":
"foo", "title": "bar", "url": "baz" } } })",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action.GetString(),
"https://foo.com/share/");
EXPECT_EQ(manifest->share_target->params->text, "foo");
EXPECT_EQ(manifest->share_target->params->title, "bar");
EXPECT_EQ(manifest->share_target->params->url, "baz");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
}
// Backwards compatibility test: Contains share_target, url_template and
// action, and action is valid, params is populated.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "url_template":
"foo.com/share?title={title}",
"action": "share/", "params": { "text":
"foo", "title": "bar", "url": "baz" } } })",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action.GetString(),
"https://foo.com/share/");
EXPECT_EQ(manifest->share_target->params->text, "foo");
EXPECT_EQ(manifest->share_target->params->title, "bar");
EXPECT_EQ(manifest->share_target->params->url, "baz");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
}
// Smoke test: Contains share_target, action and params. action is
// valid and is absolute.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
ASSERT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->action.GetString(), "https://foo.com/#");
EXPECT_TRUE(manifest->share_target->params->text.IsNull());
EXPECT_EQ(manifest->share_target->params->title, "mytitle");
EXPECT_TRUE(manifest->share_target->params->url.IsNull());
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(
"Method should be set to either GET or POST. It currently defaults to "
"GET.",
errors()[0]);
EXPECT_EQ(
"Enctype should be set to either application/x-www-form-urlencoded or "
"multipart/form-data. It currently defaults to "
"application/x-www-form-urlencoded",
errors()[1]);
}
// Return undefined if method or enctype is not string.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
10, "enctype": 10, "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"invalid method. Allowed methods are:"
"GET and POST.",
errors()[0]);
}
// Valid method and enctype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"GET", "enctype": "application/x-www-form-urlencoded",
"params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->method,
mojom::blink::ManifestShareTarget::Method::kGet);
EXPECT_EQ(manifest->share_target->enctype,
mojom::blink::ManifestShareTarget::Enctype::kFormUrlEncoded);
}
// Auto-fill in "GET" for method and "application/x-www-form-urlencoded" for
// enctype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->method,
mojom::blink::ManifestShareTarget::Method::kGet);
EXPECT_EQ(manifest->share_target->enctype,
mojom::blink::ManifestShareTarget::Enctype::kFormUrlEncoded);
}
// Invalid method values, return undefined.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"", "enctype": "application/x-www-form-urlencoded", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"invalid method. Allowed methods are:"
"GET and POST.",
errors()[0]);
}
// When method is "GET", enctype cannot be anything other than
// "application/x-www-form-urlencoded".
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"GET", "enctype": "RANDOM", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"invalid enctype. Allowed enctypes are:"
"application/x-www-form-urlencoded and multipart/form-data.",
errors()[0]);
}
// When method is "POST", enctype cannot be anything other than
// "application/x-www-form-urlencoded" or "multipart/form-data".
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "random", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"invalid enctype. Allowed enctypes are:"
"application/x-www-form-urlencoded and multipart/form-data.",
errors()[0]);
}
// Valid enctype for when method is "POST".
{
auto& manifest = ParseManifestWithURLs(
R"( { "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "application/x-www-form-urlencoded",
"params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->method,
mojom::blink::ManifestShareTarget::Method::kPost);
EXPECT_EQ(manifest->share_target->enctype,
mojom::blink::ManifestShareTarget::Enctype::kFormUrlEncoded);
EXPECT_EQ(0u, GetErrorCount());
}
// Valid enctype for when method is "POST".
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->method,
mojom::blink::ManifestShareTarget::Method::kPost);
EXPECT_EQ(manifest->share_target->enctype,
mojom::blink::ManifestShareTarget::Enctype::kMultipartFormData);
EXPECT_EQ(0u, GetErrorCount());
}
// Ascii in-sensitive.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"PosT", "enctype": "mUltIparT/Form-dAta", "params":
{ "title": "mytitle" } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->method,
mojom::blink::ManifestShareTarget::Method::kPost);
EXPECT_EQ(manifest->share_target->enctype,
mojom::blink::ManifestShareTarget::Enctype::kMultipartFormData);
EXPECT_EQ(0u, GetErrorCount());
}
// No files is okay.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [] } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_EQ(manifest->share_target->method,
mojom::blink::ManifestShareTarget::Method::kPost);
EXPECT_EQ(manifest->share_target->enctype,
mojom::blink::ManifestShareTarget::Enctype::kMultipartFormData);
EXPECT_EQ(0u, GetErrorCount());
}
// Nonempty file must have POST method and multipart/form-data enctype.
// GET method, for example, will cause an error in this case.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"GET", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": ["text/plain"]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"invalid enctype for GET method. Only "
"application/x-www-form-urlencoded is allowed.",
errors()[0]);
}
// Nonempty file must have POST method and multipart/form-data enctype.
// Enctype other than multipart/form-data will cause an error.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "application/x-www-form-urlencoded",
"params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": ["text/plain"]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("files are only supported with multipart/form-data POST.",
errors()[0]);
}
// Nonempty file must have POST method and multipart/form-data enctype.
// This case is valid.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": ["text/plain"]}] } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_TRUE(manifest->share_target->params->files.has_value());
EXPECT_EQ(1u, manifest->share_target->params->files->size());
EXPECT_EQ(0u, GetErrorCount());
}
// Invalid mimetype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": [""]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("invalid mime type inside files.", errors()[0]);
}
// Invalid mimetype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": ["helloworld"]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("invalid mime type inside files.", errors()[0]);
}
// Invalid mimetype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": ["^$/@$"]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("invalid mime type inside files.", errors()[0]);
}
// Invalid mimetype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": ["/"]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("invalid mime type inside files.", errors()[0]);
}
// Invalid mimetype.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": [" "]}] } }
})",
manifest_url, document_url);
EXPECT_FALSE(manifest->share_target.get());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("invalid mime type inside files.", errors()[0]);
}
// Accept field is empty.
{
auto& manifest = ParseManifestWithURLs(
R"({ "share_target": { "action": "https://foo.com/#", "method":
"POST", "enctype": "multipart/form-data", "params":
{ "title": "mytitle", "files": [{ "name": "name",
"accept": []}] } }
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
EXPECT_FALSE(manifest->share_target->params->files.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Accept sequence contains non-string elements.
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": [{
"name": "name",
"accept": ["image/png", 42]
}]
}
}
})",
manifest_url, document_url);
auto* share_target = manifest->share_target.get();
EXPECT_TRUE(share_target);
EXPECT_TRUE(share_target->params->files.has_value());
auto& files = share_target->params->files.value();
EXPECT_EQ(1u, files.size());
EXPECT_EQ(files[0]->name, "name");
auto& accept = files[0]->accept;
EXPECT_EQ(1u, accept.size());
EXPECT_EQ(accept[0], "image/png");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("'accept' entry ignored, expected to be of type string.",
errors()[0]);
}
// Accept is just a single string.
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": [{
"name": "name",
"accept": "image/png"
}]
}
}
})",
manifest_url, document_url);
auto* share_target = manifest->share_target.get();
EXPECT_TRUE(share_target);
EXPECT_TRUE(share_target->params->files.has_value());
auto& files = share_target->params->files.value();
EXPECT_EQ(1u, files.size());
EXPECT_EQ(files[0]->name, "name");
auto& accept = files[0]->accept;
EXPECT_EQ(1u, accept.size());
EXPECT_EQ(accept[0], "image/png");
EXPECT_EQ(0u, GetErrorCount());
}
// Accept is neither a string nor an array of strings.
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": [{
"name": "name",
"accept": true
}]
}
}
})",
manifest_url, document_url);
auto* share_target = manifest->share_target.get();
EXPECT_TRUE(share_target);
EXPECT_FALSE(share_target->params->files.has_value());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'accept' ignored, type array or string expected.",
errors()[0]);
}
// Files is just a single FileFilter (not an array).
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": {
"name": "name",
"accept": "image/png"
}
}
}
})",
manifest_url, document_url);
EXPECT_TRUE(manifest->share_target.get());
auto* params = manifest->share_target->params.get();
EXPECT_TRUE(params->files.has_value());
auto& file = params->files.value();
EXPECT_EQ(1u, file.size());
EXPECT_EQ(file[0]->name, "name");
auto& accept = file[0]->accept;
EXPECT_EQ(1u, accept.size());
EXPECT_EQ(accept[0], "image/png");
EXPECT_EQ(0u, GetErrorCount());
}
// Files is neither array nor FileFilter.
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": 3
}
}
})",
manifest_url, document_url);
auto* share_target = manifest->share_target.get();
EXPECT_TRUE(share_target);
EXPECT_FALSE(share_target->params->files.has_value());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'files' ignored, type array or FileFilter expected.",
errors()[0]);
}
// Files contains a non-dictionary entry.
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": [
{
"name": "name",
"accept": "image/png"
},
3
]
}
}
})",
manifest_url, document_url);
auto* share_target = manifest->share_target.get();
EXPECT_TRUE(share_target);
EXPECT_TRUE(share_target->params->files.has_value());
auto& files = share_target->params->files.value();
EXPECT_EQ(1u, files.size());
EXPECT_EQ(files[0]->name, "name");
auto& accept = files[0]->accept;
EXPECT_EQ(1u, accept.size());
EXPECT_EQ(accept[0], "image/png");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("files must be a sequence of non-empty file entries.",
errors()[0]);
}
// Files contains empty file.
{
auto& manifest = ParseManifestWithURLs(
R"({
"share_target": {
"action": "https://foo.com/#",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "mytitle",
"files": [
{
"name": "name",
"accept": "image/png"
},
{}
]
}
}
})",
manifest_url, document_url);
auto* share_target = manifest->share_target.get();
EXPECT_TRUE(share_target);
EXPECT_TRUE(share_target->params->files.has_value());
auto& files = share_target->params->files.value();
EXPECT_EQ(1u, files.size());
EXPECT_EQ(files[0]->name, "name");
auto& accept = files[0]->accept;
EXPECT_EQ(1u, accept.size());
EXPECT_EQ(accept[0], "image/png");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'name' missing.", errors()[0]);
}
}
TEST_F(ManifestParserTest, RelatedApplicationsParseRules) {
// If no application, empty list.
{
auto& manifest = ParseManifest(R"({ "related_applications": []})");
EXPECT_TRUE(manifest->related_applications.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// If empty application, empty list.
{
auto& manifest = ParseManifest(R"({ "related_applications": [{}]})");
EXPECT_TRUE(manifest->related_applications.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("'platform' is a required field, related application ignored.",
errors()[0]);
}
// If invalid platform, application is ignored.
{
auto& manifest =
ParseManifest(R"({ "related_applications": [{"platform": 123}]})");
EXPECT_TRUE(manifest->related_applications.empty());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'platform' ignored, type string expected.",
errors()[0]);
EXPECT_EQ(
"'platform' is a required field, "
"related application ignored.",
errors()[1]);
}
// If missing platform, application is ignored.
{
auto& manifest =
ParseManifest(R"({ "related_applications": [{"id": "foo"}]})");
EXPECT_TRUE(manifest->related_applications.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("'platform' is a required field, related application ignored.",
errors()[0]);
}
// If missing id and url, application is ignored.
{
auto& manifest =
ParseManifest(R"({ "related_applications": [{"platform": "play"}]})");
EXPECT_TRUE(manifest->related_applications.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("one of 'url' or 'id' is required, related application ignored.",
errors()[0]);
}
// Valid application, with url.
{
auto& manifest = ParseManifest(R"({ "related_applications": [
{"platform": "play", "url": "http://www.foo.com"}]})");
auto& related_applications = manifest->related_applications;
EXPECT_EQ(related_applications.size(), 1u);
EXPECT_EQ(related_applications[0]->platform, "play");
EXPECT_TRUE(related_applications[0]->url.has_value());
EXPECT_EQ(related_applications[0]->url->GetString(), "http://www.foo.com/");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Application with an invalid url.
{
auto& manifest = ParseManifest(R"({ "related_applications": [
{"platform": "play", "url": "http://www.foo.com:co&uk"}]})");
EXPECT_TRUE(manifest->related_applications.empty());
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("property 'url' ignored, URL is invalid.", errors()[0]);
EXPECT_EQ("one of 'url' or 'id' is required, related application ignored.",
errors()[1]);
}
// Valid application, with id.
{
auto& manifest = ParseManifest(R"({ "related_applications": [
{"platform": "itunes", "id": "foo"}]})");
auto& related_applications = manifest->related_applications;
EXPECT_EQ(related_applications.size(), 1u);
EXPECT_EQ(related_applications[0]->platform, "itunes");
EXPECT_EQ(related_applications[0]->id, "foo");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// All valid applications are in list.
{
auto& manifest = ParseManifest(
R"({ "related_applications": [
{"platform": "play", "id": "foo"},
{"platform": "itunes", "id": "bar"}]})");
auto& related_applications = manifest->related_applications;
EXPECT_EQ(related_applications.size(), 2u);
EXPECT_EQ(related_applications[0]->platform, "play");
EXPECT_EQ(related_applications[0]->id, "foo");
EXPECT_EQ(related_applications[1]->platform, "itunes");
EXPECT_EQ(related_applications[1]->id, "bar");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Two invalid applications and one valid. Only the valid application should
// be in the list.
{
auto& manifest = ParseManifest(
R"({ "related_applications": [
{"platform": "itunes"},
{"platform": "play", "id": "foo"},
{}]})");
auto& related_applications = manifest->related_applications;
EXPECT_EQ(related_applications.size(), 1u);
EXPECT_EQ(related_applications[0]->platform, "play");
EXPECT_EQ(related_applications[0]->id, "foo");
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("one of 'url' or 'id' is required, related application ignored.",
errors()[0]);
EXPECT_EQ("'platform' is a required field, related application ignored.",
errors()[1]);
}
}
TEST_F(ManifestParserTest, ParsePreferRelatedApplicationsParseRules) {
// Smoke test.
{
auto& manifest =
ParseManifest(R"({ "prefer_related_applications": true })");
EXPECT_TRUE(manifest->prefer_related_applications);
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if the property isn't a boolean.
{
auto& manifest = ParseManifest(R"({ "prefer_related_applications": {} })");
EXPECT_FALSE(manifest->prefer_related_applications);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'prefer_related_applications' "
"ignored, type boolean expected.",
errors()[0]);
}
{
auto& manifest =
ParseManifest(R"({ "prefer_related_applications": "true" })");
EXPECT_FALSE(manifest->prefer_related_applications);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'prefer_related_applications' "
"ignored, type boolean expected.",
errors()[0]);
}
{
auto& manifest = ParseManifest(R"({ "prefer_related_applications": 1 })");
EXPECT_FALSE(manifest->prefer_related_applications);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'prefer_related_applications' "
"ignored, type boolean expected.",
errors()[0]);
}
// "False" should set the boolean false without throwing errors.
{
auto& manifest =
ParseManifest(R"({ "prefer_related_applications": false })");
EXPECT_FALSE(manifest->prefer_related_applications);
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ThemeColorParserRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "theme_color": "#FF0000" })");
EXPECT_TRUE(manifest->has_theme_color);
EXPECT_EQ(manifest->theme_color, 0xFFFF0000u);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "theme_color": " blue " })");
EXPECT_TRUE(manifest->has_theme_color);
EXPECT_EQ(manifest->theme_color, 0xFF0000FFu);
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if theme_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "theme_color": {} })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'theme_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if theme_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "theme_color": false })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'theme_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if theme_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "theme_color": null })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'theme_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if theme_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "theme_color": [] })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'theme_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if theme_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "theme_color": 42 })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'theme_color' ignored, type string expected.",
errors()[0]);
}
// Parse fails if string is not in a known format.
{
auto& manifest = ParseManifest(R"~({ "theme_color": "foo(bar)" })~");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'theme_color' ignored,"
" 'foo(bar)' is not a valid color.",
errors()[0]);
}
// Parse fails if string is not in a known format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "bleu" })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'theme_color' ignored, 'bleu' is not a valid color.",
errors()[0]);
}
// Parse fails if string is not in a known format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "FF00FF" })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'theme_color' ignored, 'FF00FF'"
" is not a valid color.",
errors()[0]);
}
// Parse fails if multiple values for theme_color are given.
{
auto& manifest = ParseManifest(R"({ "theme_color": "#ABC #DEF" })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'theme_color' ignored, "
"'#ABC #DEF' is not a valid color.",
errors()[0]);
}
// Parse fails if multiple values for theme_color are given.
{
auto& manifest = ParseManifest(R"({ "theme_color": "#AABBCC #DDEEFF" })");
EXPECT_FALSE(manifest->has_theme_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'theme_color' ignored, "
"'#AABBCC #DDEEFF' is not a valid color.",
errors()[0]);
}
// Accept CSS color keyword format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "blue" })");
EXPECT_EQ(manifest->theme_color, 0xFF0000FFu);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS color keyword format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "chartreuse" })");
EXPECT_EQ(manifest->theme_color, 0xFF7FFF00u);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS RGB format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "#FFF" })");
EXPECT_EQ(manifest->theme_color, 0xFFFFFFFFu);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS RGB format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "#ABC" })");
EXPECT_EQ(manifest->theme_color, 0xFFAABBCCu);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS RRGGBB format.
{
auto& manifest = ParseManifest(R"({ "theme_color": "#FF0000" })");
EXPECT_EQ(manifest->theme_color, 0xFFFF0000u);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept translucent colors.
{
auto& manifest =
ParseManifest(R"~({ "theme_color": "rgba(255,0,0,0.4)" })~");
EXPECT_EQ(manifest->theme_color, 0x66FF0000u);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept transparent colors.
{
auto& manifest = ParseManifest(R"~({ "theme_color": "rgba(0,0,0,0)" })~");
EXPECT_EQ(manifest->theme_color, 0x00000000u);
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, BackgroundColorParserRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "background_color": "#FF0000" })");
EXPECT_EQ(manifest->background_color, 0xFFFF0000u);
EXPECT_FALSE(IsManifestEmpty(manifest));
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "background_color": " blue " })");
EXPECT_EQ(manifest->background_color, 0xFF0000FFu);
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if background_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "background_color": {} })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'background_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if background_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "background_color": false })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'background_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if background_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "background_color": null })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'background_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if background_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "background_color": [] })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'background_color' ignored, type string expected.",
errors()[0]);
}
// Don't parse if background_color isn't a string.
{
auto& manifest = ParseManifest(R"({ "background_color": 42 })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'background_color' ignored, type string expected.",
errors()[0]);
}
// Parse fails if string is not in a known format.
{
auto& manifest = ParseManifest(R"~({ "background_color": "foo(bar)" })~");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'background_color' ignored,"
" 'foo(bar)' is not a valid color.",
errors()[0]);
}
// Parse fails if string is not in a known format.
{
auto& manifest = ParseManifest(R"({ "background_color": "bleu" })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'background_color' ignored,"
" 'bleu' is not a valid color.",
errors()[0]);
}
// Parse fails if string is not in a known format.
{
auto& manifest = ParseManifest(R"({ "background_color": "FF00FF" })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'background_color' ignored,"
" 'FF00FF' is not a valid color.",
errors()[0]);
}
// Parse fails if multiple values for background_color are given.
{
auto& manifest = ParseManifest(R"({ "background_color": "#ABC #DEF" })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'background_color' ignored, "
"'#ABC #DEF' is not a valid color.",
errors()[0]);
}
// Parse fails if multiple values for background_color are given.
{
auto& manifest =
ParseManifest(R"({ "background_color": "#AABBCC #DDEEFF" })");
EXPECT_FALSE(manifest->has_background_color);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'background_color' ignored, "
"'#AABBCC #DDEEFF' is not a valid color.",
errors()[0]);
}
// Accept CSS color keyword format.
{
auto& manifest = ParseManifest(R"({ "background_color": "blue" })");
EXPECT_EQ(manifest->background_color, 0xFF0000FFu);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS color keyword format.
{
auto& manifest = ParseManifest(R"({ "background_color": "chartreuse" })");
EXPECT_EQ(manifest->background_color, 0xFF7FFF00u);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS RGB format.
{
auto& manifest = ParseManifest(R"({ "background_color": "#FFF" })");
EXPECT_EQ(manifest->background_color, 0xFFFFFFFFu);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS RGB format.
{
auto& manifest = ParseManifest(R"({ "background_color": "#ABC" })");
EXPECT_EQ(manifest->background_color, 0xFFAABBCCu);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept CSS RRGGBB format.
{
auto& manifest = ParseManifest(R"({ "background_color": "#FF0000" })");
EXPECT_EQ(manifest->background_color, 0xFFFF0000u);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept translucent colors.
{
auto& manifest =
ParseManifest(R"~({ "background_color": "rgba(255,0,0,0.4)" })~");
EXPECT_EQ(manifest->background_color, 0x66FF0000u);
EXPECT_EQ(0u, GetErrorCount());
}
// Accept transparent colors.
{
auto& manifest =
ParseManifest(R"~({ "background_color": "rgba(0,0,0,0)" })~");
EXPECT_EQ(manifest->background_color, 0x00000000u);
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, GCMSenderIDParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({ "gcm_sender_id": "foo" })");
EXPECT_EQ(manifest->gcm_sender_id, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({ "gcm_sender_id": " foo " })");
EXPECT_EQ(manifest->gcm_sender_id, "foo");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if the property isn't a string.
{
auto& manifest = ParseManifest(R"({ "gcm_sender_id": {} })");
EXPECT_TRUE(manifest->gcm_sender_id.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'gcm_sender_id' ignored, type string expected.",
errors()[0]);
}
{
auto& manifest = ParseManifest(R"({ "gcm_sender_id": 42 })");
EXPECT_TRUE(manifest->gcm_sender_id.IsNull());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'gcm_sender_id' ignored, type string expected.",
errors()[0]);
}
}
TEST_F(ManifestParserTest, PermissionsPolicyParsesOrigins) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["https://example.com"],
"microphone": ["https://example.com"]
}})");
EXPECT_EQ(0u, GetErrorCount());
EXPECT_EQ(2u, manifest->permissions_policy.size());
for (const auto& policy : manifest->permissions_policy) {
EXPECT_EQ(1u, policy.allowed_origins.size());
EXPECT_EQ("https://example.com", policy.allowed_origins[0].Serialize());
EXPECT_FALSE(manifest->permissions_policy[0].self_if_matches.has_value());
}
}
TEST_F(ManifestParserTest, PermissionsPolicyParsesSelf) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["self"]
}})");
EXPECT_EQ(0u, GetErrorCount());
EXPECT_EQ(1u, manifest->permissions_policy.size());
EXPECT_EQ("http://foo.com",
manifest->permissions_policy[0].self_if_matches->Serialize());
EXPECT_EQ(0u, manifest->permissions_policy[0].allowed_origins.size());
}
TEST_F(ManifestParserTest, PermissionsPolicyIgnoresSrc) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["src"]
}})");
EXPECT_EQ(0u, GetErrorCount());
EXPECT_EQ(1u, manifest->permissions_policy.size());
EXPECT_EQ(0u, manifest->permissions_policy[0].allowed_origins.size());
EXPECT_FALSE(manifest->permissions_policy[0].self_if_matches.has_value());
}
TEST_F(ManifestParserTest, PermissionsPolicyParsesNone) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["none"]
}})");
EXPECT_EQ(0u, GetErrorCount());
EXPECT_EQ(1u, manifest->permissions_policy.size());
EXPECT_EQ(0u, manifest->permissions_policy[0].allowed_origins.size());
}
TEST_F(ManifestParserTest, PermissionsPolicyParsesWildcard) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["*"]
}})");
EXPECT_EQ(0u, GetErrorCount());
EXPECT_EQ(1u, manifest->permissions_policy.size());
EXPECT_TRUE(manifest->permissions_policy[0].matches_all_origins);
}
TEST_F(ManifestParserTest, PermissionsPolicyEmptyOrigin) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["https://example.com"],
"microphone": [""],
"midi": []
}})");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(1u, manifest->permissions_policy.size());
}
TEST_F(ManifestParserTest, PermissionsPolicyAsArray) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": [
{"geolocation": ["https://example.com"]},
{"microphone": [""]},
{"midi": []}
]})");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(0u, manifest->permissions_policy.size());
EXPECT_EQ("property 'permissions_policy' ignored, type object expected.",
errors()[0]);
}
TEST_F(ManifestParserTest, PermissionsPolicyInvalidType) {
auto& manifest = ParseManifest(R"({ "permissions_policy": true})");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(0u, manifest->permissions_policy.size());
EXPECT_EQ("property 'permissions_policy' ignored, type object expected.",
errors()[0]);
}
TEST_F(ManifestParserTest, PermissionsPolicyInvalidAllowlistType) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["https://example.com"],
"microphone": 0,
"midi": true
}})");
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(1u, manifest->permissions_policy.size());
EXPECT_EQ(
"permission 'microphone' ignored, invalid allowlist: type array "
"expected.",
errors()[0]);
EXPECT_EQ(
"permission 'midi' ignored, invalid allowlist: type array expected.",
errors()[1]);
}
TEST_F(ManifestParserTest, PermissionsPolicyInvalidAllowlistEntry) {
auto& manifest = ParseManifest(
R"({ "permissions_policy": {
"geolocation": ["https://example.com", null],
"microphone": ["https://example.com", {}]
}})");
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ(0u, manifest->permissions_policy.size());
EXPECT_EQ(
"permissions_policy entry ignored, required property 'origin' contains "
"an invalid element: type string expected.",
errors()[0]);
EXPECT_EQ(
"permissions_policy entry ignored, required property 'origin' contains "
"an invalid element: type string expected.",
errors()[1]);
}
TEST_F(ManifestParserTest, LaunchHandlerParseRules) {
using ClientMode = mojom::blink::ManifestLaunchHandler::ClientMode;
// Smoke test.
{
auto& manifest = ParseManifest(R"({
"launch_handler": {
"client_mode": "focus-existing"
}
})");
EXPECT_EQ(manifest->launch_handler->client_mode,
ClientMode::kFocusExisting);
EXPECT_EQ(0u, GetErrorCount());
}
{
auto& manifest = ParseManifest(R"({
"launch_handler": {
"client_mode": "navigate-new"
}
})");
EXPECT_EQ(manifest->launch_handler->client_mode, ClientMode::kNavigateNew);
EXPECT_EQ(0u, GetErrorCount());
}
// Empty object is fine.
{
auto& manifest = ParseManifest(R"({
"launch_handler": {}
})");
EXPECT_EQ(manifest->launch_handler->client_mode, std::nullopt);
EXPECT_EQ(0u, GetErrorCount());
}
// Empty array is fine.
{
auto& manifest = ParseManifest(R"({
"launch_handler": {
"client_mode": []
}
})");
EXPECT_EQ(manifest->launch_handler->client_mode, std::nullopt);
EXPECT_EQ(0u, GetErrorCount());
}
// Unknown single string.
{
auto& manifest = ParseManifest(R"({
"launch_handler": {
"client_mode": "space"
}
})");
EXPECT_EQ(manifest->launch_handler->client_mode, std::nullopt);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("client_mode value 'space' ignored, unknown value.", errors()[0]);
}
// First known value in array is used.
{
auto& manifest = ParseManifest(R"({
"launch_handler": {
"client_mode": ["navigate-existing", "navigate-new"]
}
})");
EXPECT_EQ(manifest->launch_handler->client_mode,
ClientMode::kNavigateExisting);
EXPECT_EQ(0u, GetErrorCount());
}
{
auto& manifest = ParseManifest(R"({
"launch_handler": {
"client_mode": [null, "space", "focus-existing", "auto"]
}
})");
EXPECT_EQ(manifest->launch_handler->client_mode,
ClientMode::kFocusExisting);
EXPECT_EQ(2u, GetErrorCount());
EXPECT_EQ("client_mode value 'null' ignored, string expected.",
errors()[0]);
EXPECT_EQ("client_mode value 'space' ignored, unknown value.", errors()[1]);
}
// Don't parse if the property isn't an object.
{
auto& manifest = ParseManifest(R"({ "launch_handler": null })");
EXPECT_FALSE(manifest->launch_handler);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("launch_handler value ignored, object expected.", errors()[0]);
}
{
auto& manifest = ParseManifest(R"({
"launch_handler": [{
"client_mode": "navigate-new"
}]
})");
EXPECT_FALSE(manifest->launch_handler);
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("launch_handler value ignored, object expected.", errors()[0]);
}
}
TEST_F(ManifestParserTest, TranslationsParseRules) {
{
ScopedWebAppTranslationsForTest feature(false);
// Feature not enabled, should not be parsed.
auto& manifest =
ParseManifest(R"({ "translations": {"fr": {"name": "french name"}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(0u, GetErrorCount());
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTranslations));
}
{
ScopedWebAppTranslationsForTest feature(true);
// Manifest does not contain a 'translations' field.
{
auto& manifest = ParseManifest(R"({ })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if translations object is empty.
{
auto& manifest = ParseManifest(R"({ "translations": {} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Empty translation is ignored.
{
auto& manifest = ParseManifest(R"({ "translations": {"fr": {}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_FALSE(manifest->translations.Contains("fr"));
EXPECT_EQ(0u, GetErrorCount());
}
// Valid name, short_name and description should be parsed
{
auto& manifest = ParseManifest(
R"({ "translations": {"fr": {"name": "french name", "short_name":
"fr name", "description": "french description"}} })");
EXPECT_FALSE(manifest->translations.empty());
EXPECT_TRUE(manifest->translations.Contains("fr"));
EXPECT_EQ(manifest->translations.find("fr")->value->name, "french name");
EXPECT_EQ(manifest->translations.find("fr")->value->short_name,
"fr name");
EXPECT_EQ(manifest->translations.find("fr")->value->description,
"french description");
EXPECT_EQ(0u, GetErrorCount());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTranslations));
}
// Don't parse if the property isn't an object.
{
auto& manifest = ParseManifest(R"({ "translations": [] })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'translations' ignored, object expected.",
errors()[0]);
}
// Ignore translation if it isn't an object.
{
auto& manifest = ParseManifest(R"({ "translations": {"fr": []} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("skipping translation, object expected.", errors()[0]);
}
// Multiple valid translations should all be parsed.
{
auto& manifest = ParseManifest(
R"({ "translations": {"fr": {"name": "french name"},
"es": {"name": "spanish name"}} })");
EXPECT_FALSE(manifest->translations.empty());
EXPECT_TRUE(manifest->translations.Contains("fr"));
EXPECT_TRUE(manifest->translations.Contains("es"));
EXPECT_EQ(manifest->translations.find("fr")->value->name, "french name");
EXPECT_EQ(manifest->translations.find("es")->value->name, "spanish name");
EXPECT_EQ(0u, GetErrorCount());
}
// Empty locale string should be ignored.
{
auto& manifest = ParseManifest(
R"({ "translations": {"": {"name": "translated name"}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("skipping translation, non-empty locale string expected.",
errors()[0]);
}
}
}
TEST_F(ManifestParserTest, TranslationsStringsParseRules) {
ScopedWebAppTranslationsForTest feature(true);
// Ignore non-string translations name.
{
auto& manifest =
ParseManifest(R"({ "translations": {"fr": {"name": {}}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'name' of 'translations' ignored, type string expected.",
errors()[0]);
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTranslations));
}
// Ignore non-string translations short_name.
{
auto& manifest =
ParseManifest(R"({ "translations": {"fr": {"short_name": []}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'short_name' of 'translations' ignored, type string "
"expected.",
errors()[0]);
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTranslations));
}
// Ignore non-string translations description.
{
auto& manifest =
ParseManifest(R"({ "translations": {"fr": {"description": 42}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'description' of 'translations' ignored, type string "
"expected.",
errors()[0]);
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTranslations));
}
// Translation with empty strings is ignored.
{
auto& manifest = ParseManifest(
R"({ "translations": {"fr": {"name": "", "short_name": "",
"description": ""}} })");
EXPECT_TRUE(manifest->translations.empty());
EXPECT_FALSE(manifest->translations.Contains("fr"));
EXPECT_EQ(3u, GetErrorCount());
EXPECT_EQ("property 'name' of 'translations' is an empty string.",
errors()[0]);
EXPECT_EQ("property 'short_name' of 'translations' is an empty string.",
errors()[1]);
EXPECT_EQ("property 'description' of 'translations' is an empty string.",
errors()[2]);
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTranslations));
}
}
TEST_F(ManifestParserTest, TabStripParseRules) {
using Visibility = mojom::blink::TabStripMemberVisibility;
{
ScopedWebAppTabStripForTest feature1(true);
ScopedWebAppTabStripCustomizationsForTest feature2(false);
// Tab strip customizations feature not enabled, should not be parsed.
{
auto& manifest =
ParseManifest(R"({ "tab_strip": {"home_tab": "auto"} })");
EXPECT_TRUE(manifest->tab_strip.is_null());
EXPECT_EQ(0u, GetErrorCount());
}
EXPECT_FALSE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
}
{
ScopedWebAppTabStripForTest feature1(true);
ScopedWebAppTabStripCustomizationsForTest feature2(true);
// Display mode not 'tabbed', 'tab_strip' should still be parsed.
{
auto& manifest =
ParseManifest(R"({ "tab_strip": {"home_tab": "auto"} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_EQ(0u, GetErrorCount());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
}
// Manifest does not contain 'tab_strip' field.
{
auto& manifest = ParseManifest(R"({ "display_override": [ "tabbed" ] })");
EXPECT_TRUE(manifest->tab_strip.is_null());
EXPECT_EQ(0u, GetErrorCount());
}
// 'tab_strip' object is empty.
{
auto& manifest = ParseManifest(R"({ "tab_strip": {} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_EQ(manifest->tab_strip->home_tab->get_visibility(),
Visibility::kAuto);
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Home tab and new tab button are empty objects.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {"home_tab": {}, "new_tab_button": {}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_EQ(manifest->tab_strip->home_tab->get_params()->icons.size(), 0u);
EXPECT_EQ(
manifest->tab_strip->home_tab->get_params()->scope_patterns.size(),
0u);
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Home tab and new tab button are invalid.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {"home_tab": "something", "new_tab_button": 42} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_EQ(manifest->tab_strip->home_tab->get_visibility(),
Visibility::kAuto);
EXPECT_FALSE(manifest->tab_strip->home_tab->is_params());
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Unknown members of 'tab_strip' are ignored.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {"unknown": {}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_EQ(manifest->tab_strip->home_tab->get_visibility(),
Visibility::kAuto);
EXPECT_FALSE(manifest->tab_strip->home_tab->is_params());
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Home tab with icons and new tab button with url are parsed.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {"icons": [{"src": "foo.jpg"}]},
"new_tab_button": {"url": "foo"}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_EQ(manifest->tab_strip->home_tab->get_params()->icons.size(), 1u);
EXPECT_EQ(manifest->tab_strip->new_tab_button->url,
KURL(DefaultDocumentUrl(), "foo"));
EXPECT_EQ(0u, GetErrorCount());
}
// New tab button url out of scope.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {"new_tab_button": {"url": "https://bar.com"}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ(
"property 'url' ignored, should be within scope of the manifest.",
errors()[0]);
}
// Home tab and new tab button set to 'auto'.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {"home_tab": "auto", "new_tab_button": "auto"} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_EQ(manifest->tab_strip->home_tab->get_visibility(),
Visibility::kAuto);
EXPECT_FALSE(manifest->tab_strip->home_tab->is_params());
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Home tab set to 'absent'.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {"home_tab": "absent"} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_EQ(manifest->tab_strip->home_tab->get_visibility(),
Visibility::kAbsent);
EXPECT_FALSE(manifest->tab_strip->home_tab->is_params());
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
// Home tab with 'auto' icons and new tab button with 'auto' url.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {"icons": "auto"},
"new_tab_button": {"url": "auto"}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_EQ(manifest->tab_strip->home_tab->get_params()->icons.size(), 0u);
EXPECT_FALSE(manifest->tab_strip->new_tab_button->url.has_value());
EXPECT_EQ(0u, GetErrorCount());
}
}
}
TEST_F(ManifestParserTest, TabStripHomeTabScopeParseRules) {
ScopedWebAppTabStripForTest feature(true);
// Valid scope hostname and protocol patterns override the default manifest
// URL.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {
"scope_patterns": [
{ "protocol": "ftp" },
{ "hostname": "bar.com" },
{ "protocol": "ftp", "hostname": "bar.com" }
]
}
}
})");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_THAT(
manifest->tab_strip->home_tab->get_params()->scope_patterns,
ElementsAre(
PatternDataEq({.protocol = {"ftp"}}),
PatternDataEq({.protocol = {"http"}, .hostname = {"bar.com"}}),
PatternDataEq({.protocol = {"ftp"}, .hostname = {"bar.com"}})));
EXPECT_EQ(0u, GetErrorCount());
}
// Valid scope pathname patterns are parsed. Relative pathnames are made
// absolute, resolved relative to the manifest URL.
{
auto& manifest = ParseManifestWithURLs(
R"({
"tab_strip": {
"home_tab": {
"scope_patterns": [
{ "pathname": "foo" },
{ "pathname": "foo/bar/" },
{ "pathname": "/foo/" },
{ "pathname": "/foo/bar/" }
]
}
}
})",
KURL("http://foo.com/static/manifest.json"), DefaultDocumentUrl());
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_THAT(manifest->tab_strip->home_tab->get_params()->scope_patterns,
ElementsAre(PatternDataEq({.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/static/foo"}}),
PatternDataEq({.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/static/foo/bar/"}}),
PatternDataEq({.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/foo/"}}),
PatternDataEq({.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/foo/bar/"}})));
EXPECT_EQ(0u, GetErrorCount());
}
// Base URL provided in scope patterns is respected if it is valid.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {
"scope_patterns": [
{ "protocol": "ftp", "baseURL": "https://www.bar.com" },
{ "hostname": "bar.com", "baseURL": "https://foobar.com" },
{ "pathname": "/foo/bar/", "baseURL": "https://bar.com" },
// Invalid (expect to be discarded).
{ "pathname": "/foobar/", "baseURL": "notaurl" },
{ "pathname": "bar", "baseURL": "https://bar.com/foo" },
{ "pathname": "bar", "baseURL": "https://bar.com/foo/" }
]
}
}
})");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_THAT(manifest->tab_strip->home_tab->get_params()->scope_patterns,
ElementsAre(PatternDataEq({.protocol = {"ftp"}}),
PatternDataEq({.protocol = {"https"},
.hostname = {"bar.com"}}),
PatternDataEq({.protocol = {"https"},
.hostname = {"bar.com"},
.pathname = {"/foo/bar/"}}),
PatternDataEq({.protocol = {"https"},
.hostname = {"bar.com"},
.pathname = {"/bar"}}),
PatternDataEq({.protocol = {"https"},
.hostname = {"bar.com"},
.pathname = {"/foo/bar"}})));
EXPECT_EQ(1u, GetErrorCount());
}
// Allow patterns with wildcards and named groups in the pathname.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {
"scope_patterns": [
{ "pathname": "*" },
{ "pathname": ":foo" },
{ "pathname": "/foo/*" },
{ "pathname": "/foo/*/bar" },
{ "pathname": "/foo/:bar" },
{ "pathname": "/foo/:bar/*" }
]
}
}
})");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_THAT(
manifest->tab_strip->home_tab->get_params()->scope_patterns,
ElementsAre(PatternDataEq({
.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {PartType::kFullWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {PartType::kSegmentWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/foo", PartType::kFullWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/foo", PartType::kFullWildcard, "/bar"},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/foo", PartType::kSegmentWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"foo.com"},
.pathname = {"/foo", PartType::kSegmentWildcard,
PartType::kFullWildcard},
})));
EXPECT_EQ(0u, GetErrorCount());
}
// Allow patterns with wildcards and named groups in the hostname.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {
"scope_patterns": [
{ "hostname": "*" },
{ "hostname": "bar.com" },
{ "hostname": "bar*.com" },
{ "hostname": "bar.*" },
{ "hostname": "bar.*.com" },
{ "hostname": "foo.:bar.*" },
{ "hostname": "*.com" }
]
}
}
})");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_THAT(
manifest->tab_strip->home_tab->get_params()->scope_patterns,
ElementsAre(PatternDataEq({
.protocol = {"http"},
.hostname = {PartType::kFullWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"bar.com"},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"bar", PartType::kFullWildcard, ".com"},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"bar", PartType::kFullWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"bar", PartType::kFullWildcard, ".com"},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {"foo", PartType::kSegmentWildcard,
PartType::kFullWildcard},
}),
PatternDataEq({
.protocol = {"http"},
.hostname = {PartType::kFullWildcard, ".com"},
})));
EXPECT_EQ(0u, GetErrorCount());
}
// Reject patterns containing custom regex in any field, with errors.
{
auto& manifest = ParseManifest(R"a({
"tab_strip": {
"home_tab": {"scope_patterns":
[{"pathname": "([a-z]+)/"}, {"pathname": "/foo/([a-z]+)/"},
{"protocol": "http([a-z])+)"}, {"hostname": "([a-z]+).com"},
{"username": "([A-Za-z])+"}, {"password": "([A-Za-z0-9@%^!])+"},
{"port": "(80|443)"}, {"hash": "([a-zA-Z0-9])+"},
{"search": "([A-Za-z0-9])+"}
]}} })a");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_EQ(
manifest->tab_strip->home_tab->get_params()->scope_patterns.size(), 0u);
EXPECT_EQ(9u, GetErrorCount());
}
// Patterns list doesn't contain objects.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {"scope_patterns": ["blah", 3]}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_EQ(
manifest->tab_strip->home_tab->get_params()->scope_patterns.size(), 0u);
EXPECT_EQ(0u, GetErrorCount());
}
// Pattern list is empty.
{
auto& manifest = ParseManifest(R"({
"tab_strip": {
"home_tab": {"scope_patterns": []}} })");
EXPECT_FALSE(manifest->tab_strip.is_null());
EXPECT_TRUE(GetDocument().Loader()->GetUseCounter().IsCounted(
WebFeature::kWebAppManifestTabStrip));
EXPECT_FALSE(manifest->tab_strip->home_tab->is_visibility());
EXPECT_EQ(
manifest->tab_strip->home_tab->get_params()->scope_patterns.size(), 0u);
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, VersionParseRules) {
// Valid versions are parsed.
{
auto& manifest = ParseManifest(R"({ "version": "1.2.3" })");
EXPECT_FALSE(manifest->version.IsNull());
EXPECT_EQ(manifest->version, "1.2.3");
EXPECT_EQ(0u, GetErrorCount());
}
// Do not tamper with the version string in any way.
{
auto& manifest = ParseManifest(R"({ "version": " abc !^?$ test " })");
EXPECT_FALSE(manifest->version.IsNull());
EXPECT_EQ(manifest->version, " abc !^?$ test ");
EXPECT_EQ(0u, GetErrorCount());
}
// Reject versions that are not strings.
{
auto& manifest = ParseManifest(R"({ "version": 123 })");
EXPECT_TRUE(manifest->version.IsNull());
EXPECT_EQ(1u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, NameLocalizedParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": "English Name",
"es": "Nombre en Español"
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.size(), 2u);
EXPECT_TRUE(manifest->name_localized.Contains("en"));
EXPECT_TRUE(manifest->name_localized.Contains("es"));
EXPECT_EQ(manifest->name_localized.find("en")->value->value,
"English Name");
EXPECT_EQ(manifest->name_localized.find("es")->value->value,
"Nombre en Español");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: object format.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": {
"value": "English Name",
"lang": "en-US",
"dir": "ltr"
},
"ar": {
"value": "اسم عربي",
"lang": "ar",
"dir": "rtl"
}
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.size(), 2u);
EXPECT_EQ(manifest->name_localized.find("en")->value->value,
"English Name");
EXPECT_EQ(manifest->name_localized.find("en")->value->lang, "en-US");
EXPECT_EQ(manifest->name_localized.find("en")->value->dir,
mojom::blink::Manifest::TextDirection::kLTR);
EXPECT_EQ(manifest->name_localized.find("ar")->value->value, "اسم عربي");
EXPECT_EQ(manifest->name_localized.find("ar")->value->lang, "ar");
EXPECT_EQ(manifest->name_localized.find("ar")->value->dir,
mojom::blink::Manifest::TextDirection::kRTL);
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": " English Name ",
"es": {
"value": " Nombre en Español ",
"lang": " es-ES "
}
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.find("en")->value->value,
"English Name");
EXPECT_EQ(manifest->name_localized.find("es")->value->value,
"Nombre en Español");
EXPECT_EQ(manifest->name_localized.find("es")->value->lang, "es-ES");
EXPECT_EQ(0u, GetErrorCount());
}
// Test stripping out of \t \r and \n.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": "\t\r\nEnglish Name\t\r\n",
"es": {
"value": "\t\r\nNombre en Español\t\r\n",
"lang": "\t\r\nes-ES\t\r\n"
}
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.find("en")->value->value,
"English Name");
EXPECT_EQ(manifest->name_localized.find("es")->value->value,
"Nombre en Español");
EXPECT_EQ(manifest->name_localized.find("es")->value->lang, "es-ES");
EXPECT_EQ(0u, GetErrorCount());
}
// Empty values are ignored.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": "",
"es": "Valid Name",
"fr": {
"value": "",
"lang": "fr"
}
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.size(), 1u);
EXPECT_TRUE(manifest->name_localized.Contains("es"));
EXPECT_EQ(manifest->name_localized.find("es")->value->value, "Valid Name");
EXPECT_EQ(1u, GetErrorCount());
}
// Non-string and non-object values are ignored.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": "Valid Name",
"es": 42,
"fr": null,
"de": ["array", "value"]
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.size(), 1u);
EXPECT_TRUE(manifest->name_localized.Contains("en"));
EXPECT_EQ(manifest->name_localized.find("en")->value->value, "Valid Name");
EXPECT_EQ(0u, GetErrorCount());
}
// Missing value in object format is ignored.
{
auto& manifest = ParseManifest(R"({
"name_localized": {
"en": {
"lang": "en-US",
"dir": "ltr"
},
"es": {
"value": "Valid Name",
"lang": "es"
}
}
})");
EXPECT_FALSE(manifest->name_localized.empty());
EXPECT_EQ(manifest->name_localized.size(), 1u);
EXPECT_TRUE(manifest->name_localized.Contains("es"));
EXPECT_EQ(manifest->name_localized.find("es")->value->value, "Valid Name");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if name_localized isn't an object.
{
auto& manifest = ParseManifest(R"({ "name_localized": "not an object" })");
EXPECT_TRUE(manifest->name_localized.empty());
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if name_localized is an array.
{
auto& manifest =
ParseManifest(R"({ "name_localized": ["array", "value"] })");
EXPECT_TRUE(manifest->name_localized.empty());
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, ShortNameLocalizedParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({
"short_name_localized": {
"en": "Short",
"es": "Corto"
}
})");
EXPECT_FALSE(manifest->short_name_localized.empty());
EXPECT_EQ(manifest->short_name_localized.size(), 2u);
EXPECT_TRUE(manifest->short_name_localized.Contains("en"));
EXPECT_TRUE(manifest->short_name_localized.Contains("es"));
EXPECT_EQ(manifest->short_name_localized.find("en")->value->value, "Short");
EXPECT_EQ(manifest->short_name_localized.find("es")->value->value, "Corto");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: object format.
{
auto& manifest = ParseManifest(R"({
"short_name_localized": {
"en": {
"value": "Short",
"lang": "en-US",
"dir": "ltr"
},
"ar": {
"value": "قصير",
"lang": "ar",
"dir": "rtl"
}
}
})");
EXPECT_FALSE(manifest->short_name_localized.empty());
EXPECT_EQ(manifest->short_name_localized.size(), 2u);
EXPECT_EQ(manifest->short_name_localized.find("en")->value->value, "Short");
EXPECT_EQ(manifest->short_name_localized.find("en")->value->lang, "en-US");
EXPECT_EQ(manifest->short_name_localized.find("en")->value->dir,
mojom::blink::Manifest::TextDirection::kLTR);
EXPECT_EQ(manifest->short_name_localized.find("ar")->value->value, "قصير");
EXPECT_EQ(manifest->short_name_localized.find("ar")->value->lang, "ar");
EXPECT_EQ(manifest->short_name_localized.find("ar")->value->dir,
mojom::blink::Manifest::TextDirection::kRTL);
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({
"short_name_localized": {
"en": " Short ",
"es": {
"value": " Corto ",
"lang": " es-ES "
}
}
})");
EXPECT_FALSE(manifest->short_name_localized.empty());
EXPECT_EQ(manifest->short_name_localized.find("en")->value->value, "Short");
EXPECT_EQ(manifest->short_name_localized.find("es")->value->value, "Corto");
EXPECT_EQ(manifest->short_name_localized.find("es")->value->lang, "es-ES");
EXPECT_EQ(0u, GetErrorCount());
}
// Test stripping out of \t \r and \n.
{
auto& manifest = ParseManifest(R"({
"short_name_localized": {
"en": "\t\r\nShort\t\r\n",
"es": {
"value": "\t\r\nCorto\t\r\n",
"lang": "\t\r\nes-ES\t\r\n"
}
}
})");
EXPECT_FALSE(manifest->short_name_localized.empty());
EXPECT_EQ(manifest->short_name_localized.find("en")->value->value, "Short");
EXPECT_EQ(manifest->short_name_localized.find("es")->value->value, "Corto");
EXPECT_EQ(manifest->short_name_localized.find("es")->value->lang, "es-ES");
EXPECT_EQ(0u, GetErrorCount());
}
// Empty values are ignored.
{
auto& manifest = ParseManifest(R"({
"short_name_localized": {
"en": "",
"es": "Valid",
"fr": {
"value": "",
"lang": "fr"
}
}
})");
EXPECT_FALSE(manifest->short_name_localized.empty());
EXPECT_EQ(manifest->short_name_localized.size(), 1u);
EXPECT_TRUE(manifest->short_name_localized.Contains("es"));
EXPECT_EQ(manifest->short_name_localized.find("es")->value->value, "Valid");
EXPECT_EQ(1u, GetErrorCount());
}
// Don't parse if short_name_localized isn't an object.
{
auto& manifest =
ParseManifest(R"({ "short_name_localized": "not an object" })");
EXPECT_TRUE(manifest->short_name_localized.empty());
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, DescriptionLocalizedParseRules) {
// Smoke test.
{
auto& manifest = ParseManifest(R"({
"description_localized": {
"en": "English description",
"es": "Descripción en español"
}
})");
EXPECT_FALSE(manifest->description_localized.empty());
EXPECT_EQ(manifest->description_localized.size(), 2u);
EXPECT_TRUE(manifest->description_localized.Contains("en"));
EXPECT_TRUE(manifest->description_localized.Contains("es"));
EXPECT_EQ(manifest->description_localized.find("en")->value->value,
"English description");
EXPECT_EQ(manifest->description_localized.find("es")->value->value,
"Descripción en español");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: object format.
{
auto& manifest = ParseManifest(R"({
"description_localized": {
"en": {
"value": "English description",
"lang": "en-US",
"dir": "ltr"
},
"ar": {
"value": "وصف عربي",
"lang": "ar",
"dir": "rtl"
}
}
})");
EXPECT_FALSE(manifest->description_localized.empty());
EXPECT_EQ(manifest->description_localized.size(), 2u);
EXPECT_EQ(manifest->description_localized.find("en")->value->value,
"English description");
EXPECT_EQ(manifest->description_localized.find("en")->value->lang, "en-US");
EXPECT_EQ(manifest->description_localized.find("en")->value->dir,
mojom::blink::Manifest::TextDirection::kLTR);
EXPECT_EQ(manifest->description_localized.find("ar")->value->value,
"وصف عربي");
EXPECT_EQ(manifest->description_localized.find("ar")->value->lang, "ar");
EXPECT_EQ(manifest->description_localized.find("ar")->value->dir,
mojom::blink::Manifest::TextDirection::kRTL);
EXPECT_EQ(0u, GetErrorCount());
}
// Trim whitespaces.
{
auto& manifest = ParseManifest(R"({
"description_localized": {
"en": " English description ",
"es": {
"value": " Descripción en español ",
"lang": " es-ES "
}
}
})");
EXPECT_FALSE(manifest->description_localized.empty());
EXPECT_EQ(manifest->description_localized.find("en")->value->value,
"English description");
EXPECT_EQ(manifest->description_localized.find("es")->value->value,
"Descripción en español");
EXPECT_EQ(manifest->description_localized.find("es")->value->lang, "es-ES");
EXPECT_EQ(0u, GetErrorCount());
}
// Empty values are ignored.
{
auto& manifest = ParseManifest(R"({
"description_localized": {
"en": "",
"es": "Valid description",
"fr": {
"value": "",
"lang": "fr"
}
}
})");
EXPECT_FALSE(manifest->description_localized.empty());
EXPECT_EQ(manifest->description_localized.size(), 1u);
EXPECT_TRUE(manifest->description_localized.Contains("es"));
EXPECT_EQ(manifest->description_localized.find("es")->value->value,
"Valid description");
EXPECT_EQ(1u, GetErrorCount());
}
// Don't parse if description_localized isn't an object.
{
auto& manifest =
ParseManifest(R"({ "description_localized": "not an object" })");
EXPECT_TRUE(manifest->description_localized.empty());
EXPECT_EQ(0u, GetErrorCount());
}
}
TEST_F(ManifestParserTest, IconsLocalizedParseRules) {
// Smoke test: if one icon with valid src, it will be present in the list.
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [
{ "src": "icon-en.png", "sizes": "32x32", "type": "image/png" }
],
"es": [
{ "src": "icon-es.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 2u);
EXPECT_TRUE(manifest->icons_localized.Contains("en"));
EXPECT_TRUE(manifest->icons_localized.Contains("es"));
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("es")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src.GetString(),
"http://foo.com/icon-en.png");
EXPECT_EQ(manifest->icons_localized.find("es")->value[0]->src.GetString(),
"http://foo.com/icon-es.png");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: multiple icons per locale.
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [
{ "src": "icon-en-32.png", "sizes": "32x32", "type": "image/png" },
{ "src": "icon-en-64.png", "sizes": "64x64", "type": "image/png" }
],
"es": [
{ "src": "icon-es-32.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 2u);
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 2u);
EXPECT_EQ(manifest->icons_localized.find("es")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src.GetString(),
"http://foo.com/icon-en-32.png");
EXPECT_EQ(manifest->icons_localized.find("en")->value[1]->src.GetString(),
"http://foo.com/icon-en-64.png");
EXPECT_EQ(manifest->icons_localized.find("es")->value[0]->src.GetString(),
"http://foo.com/icon-es-32.png");
EXPECT_EQ(0u, GetErrorCount());
}
// Test that empty icon arrays are ignored
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [],
"es": [
{ "src": "icon-es.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_TRUE(manifest->icons_localized.Contains("es"));
EXPECT_FALSE(manifest->icons_localized.Contains("en"));
EXPECT_EQ(manifest->icons_localized.find("es")->value.size(), 1u);
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if empty icon, no value.
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [ {} ],
"es": [
{ "src": "icon-es.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_TRUE(manifest->icons_localized.Contains("es"));
EXPECT_FALSE(manifest->icons_localized.Contains("en"));
EXPECT_EQ(manifest->icons_localized.find("es")->value.size(), 1u);
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: icon with invalid src, no value.
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [
{ "icons": [] },
{ "src": "valid-icon.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src.GetString(),
"http://foo.com/valid-icon.png");
EXPECT_EQ(0u, GetErrorCount());
}
// Smoke test: if icon with empty src, it will be present in the list.
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [ { "src": "" } ]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src.GetString(),
"http://foo.com/manifest.json");
EXPECT_EQ(0u, GetErrorCount());
}
// Test icon src validation - non-string src property should cause icon to be
// ignored
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [
{ "src": {} },
{ "src": "valid-icon.png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src.GetString(),
"http://foo.com/valid-icon.png");
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'src' ignored, type string expected.", errors()[0]);
}
// Test comprehensive icon with all properties
{
auto& manifest = ParseManifest(R"(
{
"icons_localized": {
"en": [
{
"src": "foo.webp",
"type": "image/webp",
"sizes": "192x192"
},
{
"src": "foo.svg",
"type": "image/svg+xml",
"sizes": "144x144"
}
]
}
}
)");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 2u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src,
KURL(DefaultDocumentUrl(), "foo.webp"));
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->type,
"image/webp");
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->sizes.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->sizes[0].width(),
192);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->sizes[0].height(),
192);
EXPECT_EQ(manifest->icons_localized.find("en")->value[1]->src,
KURL(DefaultDocumentUrl(), "foo.svg"));
EXPECT_EQ(manifest->icons_localized.find("en")->value[1]->type,
"image/svg+xml");
EXPECT_EQ(manifest->icons_localized.find("en")->value[1]->sizes.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[1]->sizes[0].width(),
144);
EXPECT_EQ(manifest->icons_localized.find("en")->value[1]->sizes[0].height(),
144);
EXPECT_EQ(0u, GetErrorCount());
}
// Test that invalid icons are ignored but valid ones are kept
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": [
{ "sizes": "32x32", "type": "image/png" },
{ "src": "valid-icon.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value.size(), 1u);
EXPECT_EQ(manifest->icons_localized.find("en")->value[0]->src.GetString(),
"http://foo.com/valid-icon.png");
EXPECT_EQ(0u, GetErrorCount());
}
// Don't parse if icons_localized isn't an object
{
auto& manifest = ParseManifest(R"({ "icons_localized": "not an object" })");
EXPECT_TRUE(manifest->icons_localized.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'icons_localized' ignored, type object expected.",
errors()[0]);
}
// Don't parse if icons_localized is an array
{
auto& manifest =
ParseManifest(R"({ "icons_localized": ["array", "value"] })");
EXPECT_TRUE(manifest->icons_localized.empty());
EXPECT_EQ(1u, GetErrorCount());
EXPECT_EQ("property 'icons_localized' ignored, type object expected.",
errors()[0]);
}
// Test that non-array values for locales are ignored
{
auto& manifest = ParseManifest(R"({
"icons_localized": {
"en": "not an array",
"es": [
{ "src": "valid-icon.png", "sizes": "32x32", "type": "image/png" }
]
}
})");
EXPECT_FALSE(manifest->icons_localized.empty());
EXPECT_EQ(manifest->icons_localized.size(), 1u);
EXPECT_TRUE(manifest->icons_localized.Contains("es"));
EXPECT_FALSE(manifest->icons_localized.Contains("en"));
EXPECT_EQ(1u, GetErrorCount());
}
}
} // namespace blink