1 // Copyright 2020 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 <tuple>
6 
7 #include "base/base_paths.h"
8 #include "base/files/file_path.h"
9 #include "base/files/file_util.h"
10 #include "base/files/scoped_temp_dir.h"
11 #include "base/path_service.h"
12 #include "base/strings/stringprintf.h"
13 #include "base/test/bind.h"
14 #include "base/threading/scoped_blocking_call.h"
15 #include "content/browser/renderer_host/render_frame_host_impl.h"
16 #include "content/public/browser/web_contents.h"
17 #include "content/public/test/browser_test.h"
18 #include "content/public/test/content_browser_test.h"
19 #include "content/public/test/content_browser_test_utils.h"
20 #include "content/shell/browser/shell.h"
21 #include "mojo/public/cpp/bindings/receiver.h"
22 #include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
23 #include "services/data_decoder/public/mojom/resource_snapshot_for_web_bundle.mojom.h"
24 
25 namespace content {
26 namespace {
27 
28 const char kOnePageSimplePath[] =
29     "/web_bundle/save_page_as_web_bundle/one_page_simple.html";
30 const char kOnePageWithImgPath[] =
31     "/web_bundle/save_page_as_web_bundle/one_page_with_img.html";
32 const char kImgPngPath[] = "/web_bundle/save_page_as_web_bundle/img.png";
33 
GetResourceCount(mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle> & snapshot)34 uint64_t GetResourceCount(
35     mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle>& snapshot) {
36   uint64_t count_out = 0;
37   base::RunLoop run_loop;
38   snapshot->GetResourceCount(
39       base::BindLambdaForTesting([&run_loop, &count_out](uint64_t count) {
40         count_out = count;
41         run_loop.Quit();
42       }));
43   run_loop.Run();
44   return count_out;
45 }
46 
GetResourceInfo(mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle> & snapshot,uint64_t index)47 data_decoder::mojom::SerializedResourceInfoPtr GetResourceInfo(
48     mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle>& snapshot,
49     uint64_t index) {
50   data_decoder::mojom::SerializedResourceInfoPtr info_out;
51   base::RunLoop run_loop;
52   snapshot->GetResourceInfo(
53       index, base::BindLambdaForTesting(
54                  [&run_loop, &info_out](
55                      data_decoder::mojom::SerializedResourceInfoPtr info) {
56                    info_out = std::move(info);
57                    run_loop.Quit();
58                  }));
59   run_loop.Run();
60   return info_out;
61 }
62 
GetResourceBody(mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle> & snapshot,uint64_t index)63 base::Optional<mojo_base::BigBuffer> GetResourceBody(
64     mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle>& snapshot,
65     uint64_t index) {
66   base::Optional<mojo_base::BigBuffer> data_out;
67   base::RunLoop run_loop;
68   snapshot->GetResourceBody(
69       index,
70       base::BindLambdaForTesting(
71           [&run_loop, &data_out](base::Optional<mojo_base::BigBuffer> data) {
72             data_out = std::move(data);
73             run_loop.Quit();
74           }));
75   run_loop.Run();
76   return data_out;
77 }
78 
79 class MockWebBundler : public data_decoder::mojom::WebBundler {
80  public:
81   MockWebBundler() = default;
~MockWebBundler()82   ~MockWebBundler() override {
83     if (file_.IsValid()) {
84       base::ScopedAllowBlockingForTesting allow_blocking;
85       file_.Close();
86     }
87   }
88 
89   MockWebBundler(const MockWebBundler&) = delete;
90   MockWebBundler& operator=(const MockWebBundler&) = delete;
91 
Bind(mojo::PendingReceiver<data_decoder::mojom::WebBundler> receiver)92   void Bind(mojo::PendingReceiver<data_decoder::mojom::WebBundler> receiver) {
93     receiver_.Bind(std::move(receiver));
94   }
95 
WaitUntilGenerateCalled()96   void WaitUntilGenerateCalled() {
97     if (callback_)
98       return;
99     base::RunLoop loop;
100     generate_called_callback_ = loop.QuitClosure();
101     loop.Run();
102   }
103 
ResetReceiver()104   void ResetReceiver() { receiver_.reset(); }
105 
106  private:
107   // mojom::WebBundleParserFactory implementation.
Generate(std::vector<mojo::PendingRemote<data_decoder::mojom::ResourceSnapshotForWebBundle>> snapshots,base::File file,GenerateCallback callback)108   void Generate(
109       std::vector<mojo::PendingRemote<
110           data_decoder::mojom::ResourceSnapshotForWebBundle>> snapshots,
111       base::File file,
112       GenerateCallback callback) override {
113     DCHECK(!callback_);
114     snapshots_ = std::move(snapshots);
115     file_ = std::move(file);
116     callback_ = std::move(callback);
117     if (generate_called_callback_)
118       std::move(generate_called_callback_).Run();
119   }
120 
121   std::vector<
122       mojo::PendingRemote<data_decoder::mojom::ResourceSnapshotForWebBundle>>
123       snapshots_;
124   base::File file_;
125   GenerateCallback callback_;
126   base::OnceClosure generate_called_callback_;
127 
128   mojo::Receiver<data_decoder::mojom::WebBundler> receiver_{this};
129 };
130 
131 }  // namespace
132 
133 class SavePageAsWebBundleBrowserTest : public ContentBrowserTest {
134  protected:
SetUpOnMainThread()135   void SetUpOnMainThread() override {
136     embedded_test_server()->AddDefaultHandlers(GetTestDataFilePath());
137     ASSERT_TRUE(embedded_test_server()->Start());
138   }
139 
140   mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle>
NavigateAndGetSnapshot(const GURL & url)141   NavigateAndGetSnapshot(const GURL& url) {
142     NavigateToURLBlockUntilNavigationsComplete(shell(), url, 1);
143     mojo::Remote<data_decoder::mojom::ResourceSnapshotForWebBundle> snapshot;
144     static_cast<RenderFrameHostImpl*>(shell()->web_contents()->GetMainFrame())
145         ->GetAssociatedLocalFrame()
146         ->GetResourceSnapshotForWebBundle(
147             snapshot.BindNewPipeAndPassReceiver());
148     return snapshot;
149   }
150 
CreateSaveDir()151   bool CreateSaveDir() {
152     base::ScopedAllowBlockingForTesting allow_blocking;
153     return save_dir_.CreateUniqueTempDir();
154   }
155 
GenerateWebBundle(const base::FilePath & file_path)156   std::tuple<uint64_t, data_decoder::mojom::WebBundlerError> GenerateWebBundle(
157       const base::FilePath& file_path) {
158     uint64_t ret_file_size = 0;
159     data_decoder::mojom::WebBundlerError ret_error =
160         data_decoder::mojom::WebBundlerError::kOK;
161     base::RunLoop run_loop;
162     shell()->web_contents()->GenerateWebBundle(
163         file_path, base::BindLambdaForTesting(
164                        [&run_loop, &ret_file_size, &ret_error](
165                            uint64_t file_size,
166                            data_decoder::mojom::WebBundlerError error) {
167                          ret_file_size = file_size;
168                          ret_error = error;
169                          run_loop.Quit();
170                        }));
171     run_loop.Run();
172     return std::make_tuple(ret_file_size, ret_error);
173   }
174 
175   base::ScopedTempDir save_dir_;
176 };
177 
IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,SnapshotOnePageSimple)178 IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest, SnapshotOnePageSimple) {
179   const auto page_url = embedded_test_server()->GetURL(kOnePageSimplePath);
180   auto snapshot = NavigateAndGetSnapshot(page_url);
181   ASSERT_EQ(1u, GetResourceCount(snapshot));
182 
183   auto info = GetResourceInfo(snapshot, 0);
184   ASSERT_TRUE(info);
185   EXPECT_EQ(page_url, info->url);
186   EXPECT_EQ("text/html", info->mime_type);
187   EXPECT_GT(info->size, 0lu);
188 
189   auto data = GetResourceBody(snapshot, 0);
190   ASSERT_TRUE(data);
191   EXPECT_EQ(info->size, data->size());
192 
193   EXPECT_EQ(
194       "<html>"
195       "<head>"
196       "<meta http-equiv=\"Content-Type\" content=\"text/html; "
197       "charset=UTF-8\">\n"
198       "<title>Hello</title>\n"
199       "</head>"
200       "<body><h1>hello world</h1>\n</body></html>",
201       std::string(reinterpret_cast<const char*>(data->data()), data->size()));
202 
203   // GetResourceInfo() API with an out-of-range index should return null.
204   EXPECT_TRUE(GetResourceInfo(snapshot, 1).is_null());
205   // GetResourceBody() API with an out-of-range index should return nullopt.
206   EXPECT_FALSE(GetResourceBody(snapshot, 1).has_value());
207 }
208 
IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,SnapshotOnePageWithImg)209 IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest, SnapshotOnePageWithImg) {
210   const auto page_url = embedded_test_server()->GetURL(kOnePageWithImgPath);
211   const auto img_url = embedded_test_server()->GetURL(kImgPngPath);
212   auto snapshot = NavigateAndGetSnapshot(page_url);
213   ASSERT_EQ(2u, GetResourceCount(snapshot));
214 
215   // The first item of resources must be the page, as FrameSerializer pushes the
216   // SerializedResource of the html content in front of the deque of
217   // SerializedResources.
218   auto page_info = GetResourceInfo(snapshot, 0);
219   ASSERT_TRUE(page_info);
220   EXPECT_EQ(page_url, page_info->url);
221   EXPECT_EQ("text/html", page_info->mime_type);
222   EXPECT_GT(page_info->size, 0lu);
223 
224   auto img_info = GetResourceInfo(snapshot, 1u);
225   ASSERT_TRUE(img_info);
226   EXPECT_EQ(img_url, img_info->url);
227   EXPECT_EQ("image/png", img_info->mime_type);
228   EXPECT_GT(img_info->size, 0lu);
229 
230   auto page_data = GetResourceBody(snapshot, 0);
231   ASSERT_TRUE(page_data);
232   EXPECT_EQ(page_info->size, page_data->size());
233   EXPECT_EQ(base::StringPrintf(
234                 "<html>"
235                 "<head>"
236                 "<meta http-equiv=\"Content-Type\" content=\"text/html; "
237                 "charset=UTF-8\">\n"
238                 "<title>Hello</title>\n"
239                 "</head>"
240                 "<body>"
241                 "<img src=\"%s\">\n"
242                 "<h1>hello world</h1>\n</body></html>",
243                 img_url.spec().c_str()),
244             std::string(reinterpret_cast<const char*>(page_data->data()),
245                         page_data->size()));
246 
247   std::string img_file_data;
248   {
249     base::ScopedAllowBlockingForTesting allow_blocking;
250     base::FilePath src_dir;
251     ASSERT_TRUE(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
252     ASSERT_TRUE(base::ReadFileToString(
253         src_dir.Append(GetTestDataFilePath())
254             .Append(FILE_PATH_LITERAL(
255                 "web_bundle/save_page_as_web_bundle/img.png")),
256         &img_file_data));
257   }
258   auto img_data = GetResourceBody(snapshot, 1);
259   EXPECT_EQ(img_file_data,
260             std::string(reinterpret_cast<const char*>(img_data->data()),
261                         img_data->size()));
262 }
263 
264 // TODO(crbug.com/1040752): Implement sub frames support and add tests.
265 // TODO(crbug.com/1040752): Implement style sheet support and add tests.
266 
IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,GenerateOnePageSimpleWebBundle)267 IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,
268                        GenerateOnePageSimpleWebBundle) {
269   const auto page_url = embedded_test_server()->GetURL(kOnePageSimplePath);
270   NavigateToURLBlockUntilNavigationsComplete(shell(), page_url, 1);
271   ASSERT_TRUE(CreateSaveDir());
272   const auto file_path =
273       save_dir_.GetPath().Append(FILE_PATH_LITERAL("test.wbn"));
274   // Currently WebBundler in the data decoder service is not implemented yet,
275   // and just returns kNotImplemented.
276   // TODO(crbug.com/1040752): Implement WebBundler and update test.
277   EXPECT_EQ(
278       std::make_tuple(0, data_decoder::mojom::WebBundlerError::kNotImplemented),
279       GenerateWebBundle(file_path));
280 }
281 
IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,GenerateWebBundleInvalidFilePath)282 IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,
283                        GenerateWebBundleInvalidFilePath) {
284   const auto page_url = embedded_test_server()->GetURL(kOnePageSimplePath);
285   NavigateToURLBlockUntilNavigationsComplete(shell(), page_url, 1);
286   ASSERT_TRUE(CreateSaveDir());
287   const auto file_path = save_dir_.GetPath();
288   // Generating Web Bundle file using the existing directory path name must
289   // fail with kFileOpenFailed error.
290   EXPECT_EQ(
291       std::make_tuple(0, data_decoder::mojom::WebBundlerError::kFileOpenFailed),
292       GenerateWebBundle(file_path));
293 }
294 
IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,GenerateWebBundleConnectionError)295 IN_PROC_BROWSER_TEST_F(SavePageAsWebBundleBrowserTest,
296                        GenerateWebBundleConnectionError) {
297   data_decoder::test::InProcessDataDecoder in_process_data_decoder;
298   MockWebBundler mock_web_bundler;
299   in_process_data_decoder.service().SetWebBundlerBinderForTesting(
300       base::BindRepeating(&MockWebBundler::Bind,
301                           base::Unretained(&mock_web_bundler)));
302 
303   const auto page_url = embedded_test_server()->GetURL(kOnePageSimplePath);
304   NavigateToURLBlockUntilNavigationsComplete(shell(), page_url, 1);
305   ASSERT_TRUE(CreateSaveDir());
306   const auto file_path =
307       save_dir_.GetPath().Append(FILE_PATH_LITERAL("test.wbn"));
308   uint64_t result_file_size = 0ul;
309   data_decoder::mojom::WebBundlerError result_error =
310       data_decoder::mojom::WebBundlerError::kOK;
311 
312   base::RunLoop run_loop;
313   shell()->web_contents()->GenerateWebBundle(
314       file_path,
315       base::BindLambdaForTesting(
316           [&run_loop, &result_file_size, &result_error](
317               uint64_t file_size, data_decoder::mojom::WebBundlerError error) {
318             result_file_size = file_size;
319             result_error = error;
320             run_loop.Quit();
321           }));
322   mock_web_bundler.WaitUntilGenerateCalled();
323   mock_web_bundler.ResetReceiver();
324   run_loop.Run();
325   // When the connection to the WebBundler in the data decoder service is
326   // disconnected, the result must be kWebBundlerConnectionError.
327   EXPECT_EQ(0ULL, result_file_size);
328   EXPECT_EQ(data_decoder::mojom::WebBundlerError::kWebBundlerConnectionError,
329             result_error);
330 }
331 
332 }  // namespace content
333