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