1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/extensions/convert_web_app.h"
6 
7 #include <stddef.h>
8 
9 #include <map>
10 #include <memory>
11 #include <string>
12 #include <utility>
13 #include <vector>
14 
15 #include "base/files/file_path.h"
16 #include "base/files/file_util.h"
17 #include "base/files/scoped_temp_dir.h"
18 #include "base/path_service.h"
19 #include "base/run_loop.h"
20 #include "base/stl_util.h"
21 #include "base/strings/string16.h"
22 #include "base/strings/stringprintf.h"
23 #include "base/strings/utf_string_conversions.h"
24 #include "base/test/scoped_feature_list.h"
25 #include "base/time/time.h"
26 #include "base/version.h"
27 #include "chrome/browser/extensions/extension_service.h"
28 #include "chrome/browser/extensions/extension_service_test_base.h"
29 #include "chrome/browser/web_applications/components/web_application_info.h"
30 #include "chrome/common/chrome_features.h"
31 #include "chrome/common/chrome_paths.h"
32 #include "chrome/common/extensions/manifest_handlers/app_launch_info.h"
33 #include "chrome/common/extensions/manifest_handlers/linked_app_icons.h"
34 #include "components/services/app_service/public/cpp/file_handler.h"
35 #include "components/services/app_service/public/cpp/file_handler_info.h"
36 #include "extensions/browser/extension_system.h"
37 #include "extensions/common/extension.h"
38 #include "extensions/common/extension_icon_set.h"
39 #include "extensions/common/extension_resource.h"
40 #include "extensions/common/manifest_constants.h"
41 #include "extensions/common/manifest_handlers/file_handler_info.h"
42 #include "extensions/common/manifest_handlers/icons_handler.h"
43 #include "extensions/common/manifest_handlers/web_app_file_handler.h"
44 #include "extensions/common/manifest_handlers/web_app_linked_shortcut_items.h"
45 #include "extensions/common/manifest_handlers/web_app_shortcut_icons_handler.h"
46 #include "extensions/common/permissions/permission_set.h"
47 #include "extensions/common/permissions/permissions_data.h"
48 #include "extensions/common/url_pattern.h"
49 #include "testing/gmock/include/gmock/gmock-matchers.h"
50 #include "testing/gtest/include/gtest/gtest.h"
51 #include "ui/gfx/codec/png_codec.h"
52 #include "url/gurl.h"
53 
54 namespace extensions {
55 
56 namespace keys = manifest_keys;
57 
58 namespace {
59 
60 // Returns an icon bitmap corresponding to a canned icon size.
GetIconBitmap(int size)61 SkBitmap GetIconBitmap(int size) {
62   SkBitmap result;
63 
64   base::FilePath icon_file;
65   if (!base::PathService::Get(chrome::DIR_TEST_DATA, &icon_file)) {
66     ADD_FAILURE() << "Could not get test data directory.";
67     return result;
68   }
69 
70   icon_file = icon_file.AppendASCII("extensions")
71                        .AppendASCII("convert_web_app")
72                        .AppendASCII(base::StringPrintf("%i.png", size));
73 
74   std::string icon_data;
75   if (!base::ReadFileToString(icon_file, &icon_data)) {
76     ADD_FAILURE() << "Could not read test icon.";
77     return result;
78   }
79 
80   if (!gfx::PNGCodec::Decode(
81           reinterpret_cast<const unsigned char*>(icon_data.c_str()),
82           icon_data.size(), &result)) {
83     ADD_FAILURE() << "Could not decode test icon.";
84     return result;
85   }
86 
87   return result;
88 }
89 
GetTestTime(int year,int month,int day,int hour,int minute,int second,int millisecond)90 base::Time GetTestTime(int year, int month, int day, int hour, int minute,
91                        int second, int millisecond) {
92   base::Time::Exploded exploded = {0};
93   exploded.year = year;
94   exploded.month = month;
95   exploded.day_of_month = day;
96   exploded.hour = hour;
97   exploded.minute = minute;
98   exploded.second = second;
99   exploded.millisecond = millisecond;
100   base::Time out_time;
101   EXPECT_TRUE(base::Time::FromUTCExploded(exploded, &out_time));
102   return out_time;
103 }
104 
105 }  // namespace
106 
107 class ExtensionFromWebApp : public extensions::ExtensionServiceTestBase {
108  public:
SetUp()109   void SetUp() override {
110     extensions::ExtensionServiceTestBase::SetUp();
111     ASSERT_TRUE(extensions_dir_.CreateUniqueTempDir());
112   }
113 
StartExtensionService()114   void StartExtensionService() {
115     InitializeEmptyExtensionService();
116     service()->Init();
117     base::RunLoop().RunUntilIdle();
118     ASSERT_TRUE(ExtensionSystem::Get(service()->profile())->is_ready());
119   }
120 
ExtensionPath() const121   const base::FilePath& ExtensionPath() const {
122     return extensions_dir_.GetPath();
123   }
124 
125  private:
126   base::ScopedTempDir extensions_dir_;
127 };
128 
129 class ExtensionFromWebAppWithShortcutsMenu : public ExtensionFromWebApp {
130  public:
ExtensionFromWebAppWithShortcutsMenu()131   ExtensionFromWebAppWithShortcutsMenu() {
132     scoped_feature_list.InitAndEnableFeature(
133         features::kDesktopPWAsAppIconShortcutsMenu);
134   }
135 
136  private:
137   base::test::ScopedFeatureList scoped_feature_list;
138 };
139 
TEST_F(ExtensionFromWebApp,GetScopeURLFromBookmarkApp)140 TEST_F(ExtensionFromWebApp, GetScopeURLFromBookmarkApp) {
141   StartExtensionService();
142   base::DictionaryValue manifest;
143   manifest.SetString(keys::kName, "Test App");
144   manifest.SetString(keys::kVersion, "0");
145   manifest.SetString(keys::kLaunchWebURL, "http://aaronboodman.com/gearpad/");
146 
147   // Create a "url_handlers" dictionary with one URL handler generated from
148   // the scope.
149   // {
150   //   "scope": {
151   //     "matches": [ "http://aaronboodman.com/gearpad/*" ],
152   //     "title": "Test App"
153   //   },
154   // }
155   GURL scope_url = GURL("http://aaronboodman.com/gearpad/");
156   manifest.SetDictionary(keys::kUrlHandlers,
157                          CreateURLHandlersForBookmarkApp(
158                              scope_url, base::ASCIIToUTF16("Test App")));
159 
160   std::string error;
161   scoped_refptr<Extension> bookmark_app =
162       Extension::Create(ExtensionPath(), Manifest::INTERNAL, manifest,
163                         Extension::FROM_BOOKMARK, &error);
164   ASSERT_TRUE(bookmark_app.get());
165 
166   EXPECT_EQ(scope_url, GetScopeURLFromBookmarkApp(bookmark_app.get()));
167 }
168 
TEST_F(ExtensionFromWebApp,GetScopeURLFromBookmarkApp_NoURLHandlers)169 TEST_F(ExtensionFromWebApp, GetScopeURLFromBookmarkApp_NoURLHandlers) {
170   StartExtensionService();
171   base::DictionaryValue manifest;
172   manifest.SetString(keys::kName, "Test App");
173   manifest.SetString(keys::kVersion, "0");
174   manifest.SetString(keys::kLaunchWebURL, "http://aaronboodman.com/gearpad/");
175   manifest.SetDictionary(keys::kUrlHandlers,
176                          std::make_unique<base::DictionaryValue>());
177 
178   std::string error;
179   scoped_refptr<Extension> bookmark_app =
180       Extension::Create(ExtensionPath(), Manifest::INTERNAL, manifest,
181                         Extension::FROM_BOOKMARK, &error);
182   ASSERT_TRUE(bookmark_app.get());
183 
184   EXPECT_EQ(GURL(), GetScopeURLFromBookmarkApp(bookmark_app.get()));
185 }
186 
TEST_F(ExtensionFromWebApp,GetScopeURLFromBookmarkApp_WrongURLHandler)187 TEST_F(ExtensionFromWebApp, GetScopeURLFromBookmarkApp_WrongURLHandler) {
188   StartExtensionService();
189   base::DictionaryValue manifest;
190   manifest.SetString(keys::kName, "Test App");
191   manifest.SetString(keys::kVersion, "0");
192   manifest.SetString(keys::kLaunchWebURL, "http://aaronboodman.com/gearpad/");
193 
194   // Create a "url_handlers" dictionary with one URL handler not generated
195   // from the scope.
196   // {
197   //   "test_url_handler": {
198   //     "matches": [ "http://*.aaronboodman.com/" ],
199   //     "title": "test handler"
200   //   }
201   // }
202   auto test_matches = std::make_unique<base::ListValue>();
203   test_matches->AppendString("http://*.aaronboodman.com/");
204 
205   auto test_handler = std::make_unique<base::DictionaryValue>();
206   test_handler->SetList(keys::kMatches, std::move(test_matches));
207   test_handler->SetString(keys::kUrlHandlerTitle, "test handler");
208 
209   auto url_handlers = std::make_unique<base::DictionaryValue>();
210   url_handlers->SetDictionary("test_url_handler", std::move(test_handler));
211   manifest.SetDictionary(keys::kUrlHandlers, std::move(url_handlers));
212 
213   std::string error;
214   scoped_refptr<Extension> bookmark_app =
215       Extension::Create(ExtensionPath(), Manifest::INTERNAL, manifest,
216                         Extension::FROM_BOOKMARK, &error);
217   ASSERT_TRUE(bookmark_app.get());
218 
219   EXPECT_EQ(GURL(), GetScopeURLFromBookmarkApp(bookmark_app.get()));
220 }
221 
TEST_F(ExtensionFromWebApp,GetScopeURLFromBookmarkApp_ExtraURLHandler)222 TEST_F(ExtensionFromWebApp, GetScopeURLFromBookmarkApp_ExtraURLHandler) {
223   StartExtensionService();
224   base::DictionaryValue manifest;
225   manifest.SetString(keys::kName, "Test App");
226   manifest.SetString(keys::kVersion, "0");
227   manifest.SetString(keys::kLaunchWebURL, "http://aaronboodman.com/gearpad/");
228 
229   // Create a "url_handlers" dictionary with two URL handlers. One for
230   // the scope and and extra one for testing.
231   // {
232   //   "scope": {
233   //     "matches": [ "http://aaronboodman.com/gearpad/*" ],
234   //     "title": "Test App"
235   //   },
236   //   "test_url_handler": {
237   //     "matches": [ "http://*.aaronboodman.com/" ],
238   //     "title": "test handler"
239   //   }
240   // }
241   GURL scope_url = GURL("http://aaronboodman.com/gearpad/");
242   std::unique_ptr<base::DictionaryValue> url_handlers =
243       CreateURLHandlersForBookmarkApp(scope_url,
244                                       base::ASCIIToUTF16("Test App"));
245 
246   auto test_matches = std::make_unique<base::ListValue>();
247   test_matches->AppendString("http://*.aaronboodman.com/");
248 
249   auto test_handler = std::make_unique<base::DictionaryValue>();
250   test_handler->SetList(keys::kMatches, std::move(test_matches));
251   test_handler->SetString(keys::kUrlHandlerTitle, "test handler");
252 
253   url_handlers->SetDictionary("test_url_handler", std::move(test_handler));
254   manifest.SetDictionary(keys::kUrlHandlers, std::move(url_handlers));
255 
256   std::string error;
257   scoped_refptr<Extension> bookmark_app =
258       Extension::Create(ExtensionPath(), Manifest::INTERNAL, manifest,
259                         Extension::FROM_BOOKMARK, &error);
260   ASSERT_TRUE(bookmark_app.get());
261 
262   // Check that we can retrieve the scope even if there is an extra
263   // url handler.
264   EXPECT_EQ(scope_url, GetScopeURLFromBookmarkApp(bookmark_app.get()));
265 }
266 
TEST_F(ExtensionFromWebApp,GenerateVersion)267 TEST_F(ExtensionFromWebApp, GenerateVersion) {
268   StartExtensionService();
269   EXPECT_EQ("2010.1.1.0",
270             ConvertTimeToExtensionVersion(
271                 GetTestTime(2010, 1, 1, 0, 0, 0, 0)));
272   EXPECT_EQ("2010.12.31.22111",
273             ConvertTimeToExtensionVersion(
274                 GetTestTime(2010, 12, 31, 8, 5, 50, 500)));
275   EXPECT_EQ("2010.10.1.65535",
276             ConvertTimeToExtensionVersion(
277                 GetTestTime(2010, 10, 1, 23, 59, 59, 999)));
278 }
279 
TEST_F(ExtensionFromWebApp,Basic)280 TEST_F(ExtensionFromWebApp, Basic) {
281   StartExtensionService();
282   WebApplicationInfo web_app;
283   web_app.title = base::ASCIIToUTF16("Gearpad");
284   web_app.description =
285       base::ASCIIToUTF16("The best text editor in the universe!");
286   web_app.start_url = GURL("http://aaronboodman.com/gearpad/");
287   web_app.scope = GURL("http://aaronboodman.com/gearpad/");
288 
289   const int sizes[] = {16, 48, 128};
290   for (size_t i = 0; i < base::size(sizes); ++i) {
291     WebApplicationIconInfo icon_info;
292     icon_info.url =
293         web_app.start_url.Resolve(base::StringPrintf("%i.png", sizes[i]));
294     icon_info.square_size_px = sizes[i];
295     web_app.icon_infos.push_back(std::move(icon_info));
296     web_app.icon_bitmaps_any[sizes[i]] = GetIconBitmap(sizes[i]);
297   }
298 
299   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
300       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
301       Extension::NO_FLAGS, Manifest::INTERNAL);
302   ASSERT_TRUE(extension.get());
303 
304   base::ScopedTempDir extension_dir;
305   EXPECT_TRUE(extension_dir.Set(extension->path()));
306 
307   EXPECT_TRUE(extension->is_app());
308   EXPECT_TRUE(extension->is_hosted_app());
309   EXPECT_TRUE(extension->from_bookmark());
310   EXPECT_FALSE(extension->is_legacy_packaged_app());
311 
312   EXPECT_FALSE(extension->was_installed_by_default());
313   EXPECT_FALSE(extension->was_installed_by_oem());
314   EXPECT_FALSE(extension->from_webstore());
315   EXPECT_EQ(Manifest::INTERNAL, extension->location());
316 
317   EXPECT_EQ("zVvdNZy3Mp7CFU8JVSyXNlDuHdVLbP7fDO3TGVzj/0w=",
318             extension->public_key());
319   EXPECT_EQ("oplhagaaipaimkjlbekcdjkffijdockj", extension->id());
320   EXPECT_EQ("1978.12.11.0", extension->version().GetString());
321   EXPECT_EQ(base::UTF16ToUTF8(web_app.title), extension->name());
322   EXPECT_EQ(base::UTF16ToUTF8(web_app.description), extension->description());
323   EXPECT_EQ(web_app.start_url,
324             AppLaunchInfo::GetFullLaunchURL(extension.get()));
325   EXPECT_EQ(web_app.scope, GetScopeURLFromBookmarkApp(extension.get()));
326   EXPECT_EQ(0u,
327             extension->permissions_data()->active_permissions().apis().size());
328   ASSERT_EQ(0u, extension->web_extent().patterns().size());
329 
330   const LinkedAppIcons& linked_icons =
331       LinkedAppIcons::GetLinkedAppIcons(extension.get());
332   EXPECT_EQ(web_app.icon_infos.size(), linked_icons.icons.size());
333   for (size_t i = 0; i < web_app.icon_infos.size(); ++i) {
334     EXPECT_EQ(web_app.icon_infos[i].url, linked_icons.icons[i].url);
335     EXPECT_EQ(web_app.icon_infos[i].square_size_px, linked_icons.icons[i].size);
336   }
337 
338   EXPECT_EQ(web_app.icon_bitmaps_any.size(),
339             IconsInfo::GetIcons(extension.get()).map().size());
340   for (const std::pair<const SquareSizePx, SkBitmap>& icon :
341        web_app.icon_bitmaps_any) {
342     int size = icon.first;
343     EXPECT_EQ(base::StringPrintf("icons/%i.png", size),
344               IconsInfo::GetIcons(extension.get())
345                   .Get(size, ExtensionIconSet::MATCH_EXACTLY));
346     ExtensionResource resource = IconsInfo::GetIconResource(
347         extension.get(), size, ExtensionIconSet::MATCH_EXACTLY);
348     ASSERT_TRUE(!resource.empty());
349     EXPECT_TRUE(base::PathExists(resource.GetFilePath()));
350   }
351 }
352 
TEST_F(ExtensionFromWebApp,Minimal)353 TEST_F(ExtensionFromWebApp, Minimal) {
354   StartExtensionService();
355   WebApplicationInfo web_app;
356   web_app.title = base::ASCIIToUTF16("Gearpad");
357   web_app.start_url = GURL("http://aaronboodman.com/gearpad/");
358 
359   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
360       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
361       Extension::NO_FLAGS, Manifest::INTERNAL);
362   ASSERT_TRUE(extension.get());
363 
364   base::ScopedTempDir extension_dir;
365   EXPECT_TRUE(extension_dir.Set(extension->path()));
366 
367   EXPECT_TRUE(extension->is_app());
368   EXPECT_TRUE(extension->is_hosted_app());
369   EXPECT_TRUE(extension->from_bookmark());
370   EXPECT_FALSE(extension->is_legacy_packaged_app());
371 
372   EXPECT_FALSE(extension->was_installed_by_default());
373   EXPECT_FALSE(extension->was_installed_by_oem());
374   EXPECT_FALSE(extension->from_webstore());
375   EXPECT_EQ(Manifest::INTERNAL, extension->location());
376 
377   EXPECT_EQ("zVvdNZy3Mp7CFU8JVSyXNlDuHdVLbP7fDO3TGVzj/0w=",
378             extension->public_key());
379   EXPECT_EQ("oplhagaaipaimkjlbekcdjkffijdockj", extension->id());
380   EXPECT_EQ("1978.12.11.0", extension->version().GetString());
381   EXPECT_EQ(base::UTF16ToUTF8(web_app.title), extension->name());
382   EXPECT_EQ("", extension->description());
383   EXPECT_EQ(web_app.start_url,
384             AppLaunchInfo::GetFullLaunchURL(extension.get()));
385   EXPECT_TRUE(GetScopeURLFromBookmarkApp(extension.get()).is_empty());
386   EXPECT_EQ(0u, IconsInfo::GetIcons(extension.get()).map().size());
387   EXPECT_EQ(0u,
388             extension->permissions_data()->active_permissions().apis().size());
389   ASSERT_EQ(0u, extension->web_extent().patterns().size());
390 }
391 
TEST_F(ExtensionFromWebApp,ExtraInstallationFlags)392 TEST_F(ExtensionFromWebApp, ExtraInstallationFlags) {
393   StartExtensionService();
394   WebApplicationInfo web_app;
395   web_app.title = base::ASCIIToUTF16("Gearpad");
396   web_app.start_url = GURL("http://aaronboodman.com/gearpad/");
397 
398   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
399       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
400       Extension::FROM_WEBSTORE | Extension::WAS_INSTALLED_BY_OEM,
401       Manifest::INTERNAL);
402   ASSERT_TRUE(extension.get());
403 
404   EXPECT_TRUE(extension->is_app());
405   EXPECT_TRUE(extension->is_hosted_app());
406   EXPECT_TRUE(extension->from_bookmark());
407   EXPECT_FALSE(extension->is_legacy_packaged_app());
408 
409   EXPECT_TRUE(extension->was_installed_by_oem());
410   EXPECT_TRUE(extension->from_webstore());
411   EXPECT_FALSE(extension->was_installed_by_default());
412   EXPECT_EQ(Manifest::INTERNAL, extension->location());
413 }
414 
TEST_F(ExtensionFromWebApp,ExternalPolicyLocation)415 TEST_F(ExtensionFromWebApp, ExternalPolicyLocation) {
416   StartExtensionService();
417   WebApplicationInfo web_app;
418   web_app.title = base::ASCIIToUTF16("Gearpad");
419   web_app.start_url = GURL("http://aaronboodman.com/gearpad/");
420 
421   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
422       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
423       Extension::NO_FLAGS, Manifest::EXTERNAL_POLICY);
424   ASSERT_TRUE(extension.get());
425 
426   EXPECT_TRUE(extension->is_app());
427   EXPECT_TRUE(extension->is_hosted_app());
428   EXPECT_TRUE(extension->from_bookmark());
429   EXPECT_FALSE(extension->is_legacy_packaged_app());
430 
431   EXPECT_EQ(Manifest::EXTERNAL_POLICY, extension->location());
432 }
433 
434 // Tests that a scope not ending in "/" works correctly.
435 // The tested behavior is unexpected but is working correctly according
436 // to the Web Manifest spec. https://github.com/w3c/manifest/issues/554
TEST_F(ExtensionFromWebApp,ScopeDoesNotEndInSlash)437 TEST_F(ExtensionFromWebApp, ScopeDoesNotEndInSlash) {
438   StartExtensionService();
439   WebApplicationInfo web_app;
440   web_app.title = base::ASCIIToUTF16("Gearpad");
441   web_app.description =
442       base::ASCIIToUTF16("The best text editor in the universe!");
443   web_app.start_url = GURL("http://aaronboodman.com/gearpad/");
444   web_app.scope = GURL("http://aaronboodman.com/gear");
445 
446   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
447       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
448       Extension::NO_FLAGS, Manifest::INTERNAL);
449   ASSERT_TRUE(extension.get());
450   EXPECT_EQ(web_app.scope, GetScopeURLFromBookmarkApp(extension.get()));
451 }
452 
453 // Tests that |file_handler| on the WebAppManifest is correctly converted
454 // to |file_handlers| on an extension manifest.
TEST_F(ExtensionFromWebApp,FileHandlersAreCorrectlyConverted)455 TEST_F(ExtensionFromWebApp, FileHandlersAreCorrectlyConverted) {
456   StartExtensionService();
457   WebApplicationInfo web_app;
458   web_app.title = base::ASCIIToUTF16("Graphr");
459   web_app.description = base::ASCIIToUTF16("A magical graphy thing");
460   web_app.start_url = GURL("https://graphr.n/");
461   web_app.scope = GURL("https://graphr.n/");
462 
463   {
464     blink::Manifest::FileHandler graph;
465     graph.action = GURL("https://graphr.n/open-graph/");
466     graph.name = base::ASCIIToUTF16("Graph");
467     graph.accept[base::ASCIIToUTF16("text/svg+xml")].push_back(
468         base::ASCIIToUTF16(""));
469     graph.accept[base::ASCIIToUTF16("text/svg+xml")].push_back(
470         base::ASCIIToUTF16(".svg"));
471     web_app.file_handlers.push_back(graph);
472 
473     blink::Manifest::FileHandler raw;
474     raw.action = GURL("https://graphr.n/open-raw/");
475     raw.name = base::ASCIIToUTF16("Raw");
476     raw.accept[base::ASCIIToUTF16("text/csv")].push_back(
477         base::ASCIIToUTF16(".csv"));
478     web_app.file_handlers.push_back(raw);
479   }
480 
481   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
482       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
483       Extension::NO_FLAGS, Manifest::INTERNAL);
484 
485   ASSERT_TRUE(extension.get());
486 
487   const std::vector<apps::FileHandlerInfo>* file_handler_infos =
488       extensions::FileHandlers::GetFileHandlers(extension.get());
489 
490   ASSERT_TRUE(file_handler_infos);
491   EXPECT_EQ(2u, file_handler_infos->size());
492 
493   {
494     const apps::FileHandlerInfo& info = file_handler_infos->at(0);
495     EXPECT_EQ("https://graphr.n/open-graph/", info.id);
496     EXPECT_FALSE(info.include_directories);
497     EXPECT_EQ(apps::file_handler_verbs::kOpenWith, info.verb);
498     // Extensions should contain SVG, and only SVG
499     EXPECT_THAT(info.extensions, testing::UnorderedElementsAre("svg"));
500     // Mime types should contain text/svg+xml and only text/svg+xml
501     EXPECT_THAT(info.types, testing::UnorderedElementsAre("text/svg+xml"));
502   }
503   {
504     const apps::FileHandlerInfo& info = file_handler_infos->at(1);
505     EXPECT_EQ("https://graphr.n/open-raw/", info.id);
506     EXPECT_FALSE(info.include_directories);
507     EXPECT_EQ(apps::file_handler_verbs::kOpenWith, info.verb);
508     // Extensions should contain csv, and only csv
509     EXPECT_THAT(info.extensions, testing::UnorderedElementsAre("csv"));
510     // Mime types should contain text/csv and only text/csv
511     EXPECT_THAT(info.types, testing::UnorderedElementsAre("text/csv"));
512   }
513 }
514 
515 // Tests that |file_handler| on the WebAppManifest is correctly converted
516 // to |web_app_file_handlers| on an extension manifest.
TEST_F(ExtensionFromWebApp,WebAppFileHandlersAreCorrectlyConverted)517 TEST_F(ExtensionFromWebApp, WebAppFileHandlersAreCorrectlyConverted) {
518   StartExtensionService();
519   WebApplicationInfo web_app;
520   web_app.title = base::ASCIIToUTF16("Graphr");
521   web_app.description = base::ASCIIToUTF16("A magical graphy thing.");
522   web_app.start_url = GURL("https://graphr.n/");
523   web_app.scope = GURL("https://graphr.n");
524 
525   {
526     blink::Manifest::FileHandler file_handler;
527     file_handler.action = GURL("https://graphr.n/open-graph/");
528     file_handler.name = base::ASCIIToUTF16("Graph");
529     file_handler.accept[base::ASCIIToUTF16("text/svg+xml")].push_back(
530         base::ASCIIToUTF16(""));
531     file_handler.accept[base::ASCIIToUTF16("text/svg+xml")].push_back(
532         base::ASCIIToUTF16(".svg"));
533     web_app.file_handlers.push_back(file_handler);
534   }
535   {
536     blink::Manifest::FileHandler file_handler;
537     file_handler.action = GURL("https://graphr.n/open-raw/");
538     file_handler.name = base::ASCIIToUTF16("Raw");
539     file_handler.accept[base::ASCIIToUTF16("text/csv")].push_back(
540         base::ASCIIToUTF16(".csv"));
541     web_app.file_handlers.push_back(file_handler);
542   }
543 
544   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
545       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
546       Extension::NO_FLAGS, Manifest::INTERNAL);
547 
548   ASSERT_TRUE(extension.get());
549 
550   const apps::FileHandlers* file_handlers =
551       extensions::WebAppFileHandlers::GetWebAppFileHandlers(extension.get());
552 
553   ASSERT_TRUE(file_handlers);
554   EXPECT_EQ(2u, file_handlers->size());
555 
556   {
557     const apps::FileHandler& file_handler = file_handlers->at(0);
558     EXPECT_EQ("https://graphr.n/open-graph/", file_handler.action);
559     EXPECT_EQ(1u, file_handler.accept.size());
560     EXPECT_EQ("text/svg+xml", file_handler.accept[0].mime_type);
561     EXPECT_THAT(file_handler.accept[0].file_extensions,
562                 testing::UnorderedElementsAre(".svg"));
563   }
564   {
565     const apps::FileHandler& file_handler = file_handlers->at(1);
566     EXPECT_EQ("https://graphr.n/open-raw/", file_handler.action);
567     EXPECT_EQ(1u, file_handler.accept.size());
568     EXPECT_EQ("text/csv", file_handler.accept[0].mime_type);
569     EXPECT_THAT(file_handler.accept[0].file_extensions,
570                 testing::UnorderedElementsAre(".csv"));
571   }
572 }
573 
574 // Tests that |shortcuts_menu_item_infos| on the WebAppManifest is correctly
575 // converted to |web_app_shortcut_icons| and |web_app_linked_shortcut_items| on
576 // an extension manifest.
TEST_F(ExtensionFromWebAppWithShortcutsMenu,WebAppShortcutIconsAreCorrectlyConverted)577 TEST_F(ExtensionFromWebAppWithShortcutsMenu,
578        WebAppShortcutIconsAreCorrectlyConverted) {
579   StartExtensionService();
580   WebApplicationInfo web_app;
581   WebApplicationShortcutsMenuItemInfo shortcut_item;
582   std::map<SquareSizePx, SkBitmap> shortcut_icon_bitmaps;
583   web_app.title = base::ASCIIToUTF16("Shortcut App");
584   web_app.description = base::ASCIIToUTF16("We have shortcuts.");
585   web_app.start_url = GURL("https://shortcut-app.io/");
586   web_app.scope = GURL("https://shortcut-app.io");
587 
588   shortcut_item.name = base::ASCIIToUTF16("Shortcut 1");
589   shortcut_item.url = GURL("https://shortcut-app.io/shortcuts/shortcut1");
590   {
591     const int sizes[] = {16, 128};
592     for (const auto& size : sizes) {
593       WebApplicationShortcutsMenuItemInfo::Icon icon_info;
594       icon_info.url = web_app.start_url.Resolve(
595           base::StringPrintf("shortcut1/%i.png", size));
596       icon_info.square_size_px = size;
597       shortcut_item.shortcut_icon_infos.push_back(std::move(icon_info));
598       shortcut_icon_bitmaps[size] = GetIconBitmap(size);
599     }
600     web_app.shortcuts_menu_icons_bitmaps.emplace_back(
601         std::move(shortcut_icon_bitmaps));
602   }
603   web_app.shortcuts_menu_item_infos.push_back(std::move(shortcut_item));
604 
605   shortcut_item.name = base::ASCIIToUTF16("Shortcut 2");
606   shortcut_item.url = GURL("https://shortcut-app.io/shortcuts/shortcut2");
607   {
608     const int sizes[] = {16, 48};
609     for (const auto& size : sizes) {
610       WebApplicationShortcutsMenuItemInfo::Icon icon_info;
611       icon_info.url =
612           web_app.start_url.Resolve(base::StringPrintf("0/%i.png", size));
613       icon_info.square_size_px = size;
614       shortcut_item.shortcut_icon_infos.push_back(std::move(icon_info));
615       shortcut_icon_bitmaps[size] = GetIconBitmap(size);
616     }
617     web_app.shortcuts_menu_icons_bitmaps.emplace_back(
618         std::move(shortcut_icon_bitmaps));
619   }
620   web_app.shortcuts_menu_item_infos.push_back(std::move(shortcut_item));
621 
622   scoped_refptr<Extension> extension = ConvertWebAppToExtension(
623       web_app, GetTestTime(1978, 12, 11, 0, 0, 0, 0), ExtensionPath(),
624       Extension::FROM_BOOKMARK, Manifest::INTERNAL);
625 
626   ASSERT_TRUE(extension.get());
627 
628   const WebAppLinkedShortcutItems& linked_shortcut_items =
629       WebAppLinkedShortcutItems::GetWebAppLinkedShortcutItems(extension.get());
630   const std::map<int, ExtensionIconSet>& shortcut_icons =
631       WebAppShortcutIconsInfo::GetShortcutIcons(extension.get());
632   for (size_t i = 0; i < web_app.shortcuts_menu_item_infos.size(); ++i) {
633     const std::vector<WebApplicationShortcutsMenuItemInfo::Icon>& icon_infos =
634         web_app.shortcuts_menu_item_infos[i].shortcut_icon_infos;
635     const std::vector<WebAppLinkedShortcutItems::ShortcutItemInfo::IconInfo>&
636         linked_shortcut_icons_info =
637             linked_shortcut_items.shortcut_item_infos[i]
638                 .shortcut_item_icon_infos;
639     ASSERT_EQ(icon_infos.size(), linked_shortcut_icons_info.size());
640     for (size_t j = 0; j < icon_infos.size(); ++j) {
641       EXPECT_EQ(linked_shortcut_icons_info[j].url, icon_infos[j].url);
642       EXPECT_EQ(linked_shortcut_icons_info[j].size,
643                 icon_infos[j].square_size_px);
644     }
645 
646     const std::map<SquareSizePx, SkBitmap>& icon_bitmaps =
647         web_app.shortcuts_menu_icons_bitmaps[i];
648     EXPECT_EQ(icon_bitmaps.size(), shortcut_icons.at(i).map().size());
649     for (const std::pair<const SquareSizePx, SkBitmap>& icon : icon_bitmaps) {
650       int size = icon.first;
651       EXPECT_EQ(
652           base::StringPrintf("shortcut_icons/%i/%i.png", static_cast<int>(i),
653                              size),
654           shortcut_icons.at(i).Get(size, ExtensionIconSet::MATCH_EXACTLY));
655 
656       ExtensionResource resource = WebAppShortcutIconsInfo::GetIconResource(
657           extension.get(), i, size, ExtensionIconSet::MATCH_EXACTLY);
658       EXPECT_TRUE(base::PathExists(resource.GetFilePath()));
659       ASSERT_TRUE(!resource.empty());
660     }
661   }
662 }
663 
664 }  // namespace extensions
665