1 // Copyright 2015 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 "components/offline_pages/core/offline_page_metadata_store.h"
6 
7 #include <stdint.h>
8 #include <memory>
9 #include <set>
10 #include <string>
11 #include <utility>
12 
13 #include "base/bind.h"
14 #include "base/files/file_path.h"
15 #include "base/files/scoped_temp_dir.h"
16 #include "base/memory/ref_counted.h"
17 #include "base/strings/string_number_conversions.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "base/test/bind.h"
20 #include "base/test/test_mock_time_task_runner.h"
21 #include "base/threading/thread_task_runner_handle.h"
22 #include "components/offline_pages/core/client_namespace_constants.h"
23 #include "components/offline_pages/core/model/offline_page_item_generator.h"
24 #include "components/offline_pages/core/offline_clock.h"
25 #include "components/offline_pages/core/offline_page_item.h"
26 #include "components/offline_pages/core/offline_page_model.h"
27 #include "components/offline_pages/core/offline_page_visuals.h"
28 #include "components/offline_pages/core/offline_store_utils.h"
29 #include "sql/database.h"
30 #include "sql/meta_table.h"
31 #include "sql/statement.h"
32 #include "sql/transaction.h"
33 #include "testing/gtest/include/gtest/gtest.h"
34 
35 namespace offline_pages {
36 
37 namespace {
38 using InitializationStatus = SqlStoreBase::InitializationStatus;
39 using OfflinePageSet = std::set<OfflinePageItem>;
40 
41 #define OFFLINE_PAGES_TABLE_V1 "offlinepages_v1"
42 
43 const char kTestClientNamespace[] = "CLIENT_NAMESPACE";
44 const char kTestURL[] = "https://example.com";
45 const char kOriginalTestURL[] = "https://example.com/foo";
46 const ClientId kTestClientId1(kTestClientNamespace, "1234");
47 const ClientId kTestClientId2(kTestClientNamespace, "5678");
48 const base::FilePath::CharType kFilePath[] =
49     FILE_PATH_LITERAL("/offline_pages/example_com.mhtml");
50 int64_t kFileSize = 234567LL;
51 int64_t kOfflineId = 12345LL;
52 const char kTestRequestOrigin[] = "request.origin";
53 int64_t kTestSystemDownloadId = 42LL;
54 const char kTestDigest[] = "test-digest";
55 const base::Time kVisualsExpiration = store_utils::FromDatabaseTime(42);
56 const char kTestSnippet[] = "test snippet";
57 const char kTestAttribution[] = "test attribution";
58 
TestVisuals()59 OfflinePageVisuals TestVisuals() {
60   return {1, base::Time(), "abc", "123"};
61 }
62 
63 // Build a store with outdated schema to simulate the upgrading process.
BuildTestStoreWithSchemaFromM52(const base::FilePath & file)64 void BuildTestStoreWithSchemaFromM52(const base::FilePath& file) {
65   sql::Database connection;
66   ASSERT_TRUE(
67       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
68   ASSERT_TRUE(connection.is_open());
69   ASSERT_TRUE(connection.BeginTransaction());
70   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
71                                  "(offline_id INTEGER PRIMARY KEY NOT NULL, "
72                                  "creation_time INTEGER NOT NULL, "
73                                  "file_size INTEGER NOT NULL, "
74                                  "version INTEGER NOT NULL, "
75                                  "last_access_time INTEGER NOT NULL, "
76                                  "access_count INTEGER NOT NULL, "
77                                  "status INTEGER NOT NULL DEFAULT 0, "
78                                  "user_initiated INTEGER, "
79                                  "client_namespace VARCHAR NOT NULL, "
80                                  "client_id VARCHAR NOT NULL, "
81                                  "online_url VARCHAR NOT NULL, "
82                                  "offline_url VARCHAR NOT NULL DEFAULT '', "
83                                  "file_path VARCHAR NOT NULL "
84                                  ")"));
85   ASSERT_TRUE(connection.CommitTransaction());
86   sql::Statement statement(connection.GetUniqueStatement(
87       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
88       "(offline_id, creation_time, file_size, version, "
89       "last_access_time, access_count, client_namespace, "
90       "client_id, online_url, file_path) "
91       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
92   statement.BindInt64(0, kOfflineId);
93   statement.BindInt(1, 0);
94   statement.BindInt64(2, kFileSize);
95   statement.BindInt(3, 0);
96   statement.BindInt(4, 0);
97   statement.BindInt(5, 1);
98   statement.BindCString(6, kTestClientNamespace);
99   statement.BindString(7, kTestClientId2.id);
100   statement.BindCString(8, kTestURL);
101   statement.BindString(9, base::FilePath(kFilePath).MaybeAsASCII());
102   ASSERT_TRUE(statement.Run());
103   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
104   ASSERT_FALSE(
105       connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "expiration_time"));
106 }
107 
BuildTestStoreWithSchemaFromM53(const base::FilePath & file)108 void BuildTestStoreWithSchemaFromM53(const base::FilePath& file) {
109   sql::Database connection;
110   ASSERT_TRUE(
111       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
112   ASSERT_TRUE(connection.is_open());
113   ASSERT_TRUE(connection.BeginTransaction());
114   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
115                                  "(offline_id INTEGER PRIMARY KEY NOT NULL, "
116                                  "creation_time INTEGER NOT NULL, "
117                                  "file_size INTEGER NOT NULL, "
118                                  "version INTEGER NOT NULL, "
119                                  "last_access_time INTEGER NOT NULL, "
120                                  "access_count INTEGER NOT NULL, "
121                                  "status INTEGER NOT NULL DEFAULT 0, "
122                                  "user_initiated INTEGER, "
123                                  "expiration_time INTEGER NOT NULL DEFAULT 0, "
124                                  "client_namespace VARCHAR NOT NULL, "
125                                  "client_id VARCHAR NOT NULL, "
126                                  "online_url VARCHAR NOT NULL, "
127                                  "offline_url VARCHAR NOT NULL DEFAULT '', "
128                                  "file_path VARCHAR NOT NULL "
129                                  ")"));
130   ASSERT_TRUE(connection.CommitTransaction());
131   sql::Statement statement(connection.GetUniqueStatement(
132       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
133       "(offline_id, creation_time, file_size, version, "
134       "last_access_time, access_count, client_namespace, "
135       "client_id, online_url, file_path, expiration_time) "
136       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
137   statement.BindInt64(0, kOfflineId);
138   statement.BindInt(1, 0);
139   statement.BindInt64(2, kFileSize);
140   statement.BindInt(3, 0);
141   statement.BindInt(4, 0);
142   statement.BindInt(5, 1);
143   statement.BindCString(6, kTestClientNamespace);
144   statement.BindString(7, kTestClientId2.id);
145   statement.BindCString(8, kTestURL);
146   statement.BindString(9, base::FilePath(kFilePath).MaybeAsASCII());
147   statement.BindInt64(10, store_utils::ToDatabaseTime(OfflineTimeNow()));
148   ASSERT_TRUE(statement.Run());
149   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
150   ASSERT_FALSE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "title"));
151 }
152 
BuildTestStoreWithSchemaFromM54(const base::FilePath & file)153 void BuildTestStoreWithSchemaFromM54(const base::FilePath& file) {
154   sql::Database connection;
155   ASSERT_TRUE(
156       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
157   ASSERT_TRUE(connection.is_open());
158   ASSERT_TRUE(connection.BeginTransaction());
159   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
160                                  "(offline_id INTEGER PRIMARY KEY NOT NULL, "
161                                  "creation_time INTEGER NOT NULL, "
162                                  "file_size INTEGER NOT NULL, "
163                                  "version INTEGER NOT NULL, "
164                                  "last_access_time INTEGER NOT NULL, "
165                                  "access_count INTEGER NOT NULL, "
166                                  "status INTEGER NOT NULL DEFAULT 0, "
167                                  "user_initiated INTEGER, "
168                                  "expiration_time INTEGER NOT NULL DEFAULT 0, "
169                                  "client_namespace VARCHAR NOT NULL, "
170                                  "client_id VARCHAR NOT NULL, "
171                                  "online_url VARCHAR NOT NULL, "
172                                  "offline_url VARCHAR NOT NULL DEFAULT '', "
173                                  "file_path VARCHAR NOT NULL, "
174                                  "title VARCHAR NOT NULL DEFAULT ''"
175                                  ")"));
176   ASSERT_TRUE(connection.CommitTransaction());
177   sql::Statement statement(connection.GetUniqueStatement(
178       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
179       "(offline_id, creation_time, file_size, version, "
180       "last_access_time, access_count, client_namespace, "
181       "client_id, online_url, file_path, expiration_time, title) "
182       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
183   statement.BindInt64(0, kOfflineId);
184   statement.BindInt(1, 0);
185   statement.BindInt64(2, kFileSize);
186   statement.BindInt(3, 0);
187   statement.BindInt(4, 0);
188   statement.BindInt(5, 1);
189   statement.BindCString(6, kTestClientNamespace);
190   statement.BindString(7, kTestClientId2.id);
191   statement.BindCString(8, kTestURL);
192   statement.BindString(9, base::FilePath(kFilePath).MaybeAsASCII());
193   statement.BindInt64(10, store_utils::ToDatabaseTime(OfflineTimeNow()));
194   statement.BindString16(11, base::UTF8ToUTF16("Test title"));
195   ASSERT_TRUE(statement.Run());
196   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
197   ASSERT_TRUE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "version"));
198   ASSERT_TRUE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "status"));
199   ASSERT_TRUE(
200       connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "user_initiated"));
201   ASSERT_TRUE(
202       connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "offline_url"));
203 }
204 
BuildTestStoreWithSchemaFromM55(const base::FilePath & file)205 void BuildTestStoreWithSchemaFromM55(const base::FilePath& file) {
206   sql::Database connection;
207   ASSERT_TRUE(
208       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
209   ASSERT_TRUE(connection.is_open());
210   ASSERT_TRUE(connection.BeginTransaction());
211   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
212                                  "(offline_id INTEGER PRIMARY KEY NOT NULL, "
213                                  "creation_time INTEGER NOT NULL, "
214                                  "file_size INTEGER NOT NULL, "
215                                  "last_access_time INTEGER NOT NULL, "
216                                  "access_count INTEGER NOT NULL, "
217                                  "expiration_time INTEGER NOT NULL DEFAULT 0, "
218                                  "client_namespace VARCHAR NOT NULL, "
219                                  "client_id VARCHAR NOT NULL, "
220                                  "online_url VARCHAR NOT NULL, "
221                                  "file_path VARCHAR NOT NULL, "
222                                  "title VARCHAR NOT NULL DEFAULT ''"
223                                  ")"));
224   ASSERT_TRUE(connection.CommitTransaction());
225   sql::Statement statement(connection.GetUniqueStatement(
226       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
227       "(offline_id, creation_time, file_size, "
228       "last_access_time, access_count, client_namespace, "
229       "client_id, online_url, file_path, expiration_time, title) "
230       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
231   statement.BindInt64(0, kOfflineId);
232   statement.BindInt(1, 0);
233   statement.BindInt64(2, kFileSize);
234   statement.BindInt(3, 0);
235   statement.BindInt(4, 1);
236   statement.BindCString(5, kTestClientNamespace);
237   statement.BindString(6, kTestClientId2.id);
238   statement.BindCString(7, kTestURL);
239   statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
240   statement.BindInt64(9, store_utils::ToDatabaseTime(OfflineTimeNow()));
241   statement.BindString16(10, base::UTF8ToUTF16("Test title"));
242   ASSERT_TRUE(statement.Run());
243   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
244   ASSERT_TRUE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "title"));
245   ASSERT_FALSE(
246       connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "original_url"));
247 }
248 
BuildTestStoreWithSchemaFromM56(const base::FilePath & file)249 void BuildTestStoreWithSchemaFromM56(const base::FilePath& file) {
250   sql::Database connection;
251   ASSERT_TRUE(
252       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
253   ASSERT_TRUE(connection.is_open());
254   ASSERT_TRUE(connection.BeginTransaction());
255   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
256                                  "(offline_id INTEGER PRIMARY KEY NOT NULL, "
257                                  "creation_time INTEGER NOT NULL, "
258                                  "file_size INTEGER NOT NULL, "
259                                  "last_access_time INTEGER NOT NULL, "
260                                  "access_count INTEGER NOT NULL, "
261                                  "expiration_time INTEGER NOT NULL DEFAULT 0, "
262                                  "client_namespace VARCHAR NOT NULL, "
263                                  "client_id VARCHAR NOT NULL, "
264                                  "online_url VARCHAR NOT NULL, "
265                                  "file_path VARCHAR NOT NULL, "
266                                  "title VARCHAR NOT NULL DEFAULT '', "
267                                  "original_url VARCHAR NOT NULL DEFAULT ''"
268                                  ")"));
269   ASSERT_TRUE(connection.CommitTransaction());
270   sql::Statement statement(connection.GetUniqueStatement(
271       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
272       "(offline_id, creation_time, file_size, "
273       "last_access_time, access_count, client_namespace, "
274       "client_id, online_url, file_path, expiration_time, title, original_url) "
275       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
276   statement.BindInt64(0, kOfflineId);
277   statement.BindInt(1, 0);
278   statement.BindInt64(2, kFileSize);
279   statement.BindInt(3, 0);
280   statement.BindInt(4, 1);
281   statement.BindCString(5, kTestClientNamespace);
282   statement.BindString(6, kTestClientId2.id);
283   statement.BindCString(7, kTestURL);
284   statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
285   statement.BindInt64(9, store_utils::ToDatabaseTime(OfflineTimeNow()));
286   statement.BindString16(10, base::UTF8ToUTF16("Test title"));
287   statement.BindCString(11, kOriginalTestURL);
288   ASSERT_TRUE(statement.Run());
289   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
290   ASSERT_TRUE(
291       connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "expiration_time"));
292 }
293 
BuildTestStoreWithSchemaFromM57(const base::FilePath & file)294 void BuildTestStoreWithSchemaFromM57(const base::FilePath& file) {
295   sql::Database connection;
296   ASSERT_TRUE(
297       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
298   ASSERT_TRUE(connection.is_open());
299   ASSERT_TRUE(connection.BeginTransaction());
300   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
301                                  "(offline_id INTEGER PRIMARY KEY NOT NULL,"
302                                  " creation_time INTEGER NOT NULL,"
303                                  " file_size INTEGER NOT NULL,"
304                                  " last_access_time INTEGER NOT NULL,"
305                                  " access_count INTEGER NOT NULL,"
306                                  " client_namespace VARCHAR NOT NULL,"
307                                  " client_id VARCHAR NOT NULL,"
308                                  " online_url VARCHAR NOT NULL,"
309                                  " file_path VARCHAR NOT NULL,"
310                                  " title VARCHAR NOT NULL DEFAULT '',"
311                                  " original_url VARCHAR NOT NULL DEFAULT ''"
312                                  ")"));
313   ASSERT_TRUE(connection.CommitTransaction());
314   sql::Statement statement(connection.GetUniqueStatement(
315       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
316       "(offline_id, creation_time, file_size, "
317       "last_access_time, access_count, client_namespace, "
318       "client_id, online_url, file_path, title, original_url) "
319       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
320   statement.BindInt64(0, kOfflineId);
321   statement.BindInt(1, 0);
322   statement.BindInt64(2, kFileSize);
323   statement.BindInt(3, 0);
324   statement.BindInt(4, 1);
325   statement.BindCString(5, kTestClientNamespace);
326   statement.BindString(6, kTestClientId2.id);
327   statement.BindCString(7, kTestURL);
328   statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
329   statement.BindString16(9, base::UTF8ToUTF16("Test title"));
330   statement.BindCString(10, kOriginalTestURL);
331   ASSERT_TRUE(statement.Run());
332   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
333   ASSERT_FALSE(
334       connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "request_origin"));
335 }
336 
BuildTestStoreWithSchemaFromM61(const base::FilePath & file)337 void BuildTestStoreWithSchemaFromM61(const base::FilePath& file) {
338   sql::Database connection;
339   ASSERT_TRUE(
340       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
341   ASSERT_TRUE(connection.is_open());
342   ASSERT_TRUE(connection.BeginTransaction());
343   ASSERT_TRUE(connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
344                                  "(offline_id INTEGER PRIMARY KEY NOT NULL,"
345                                  " creation_time INTEGER NOT NULL,"
346                                  " file_size INTEGER NOT NULL,"
347                                  " last_access_time INTEGER NOT NULL,"
348                                  " access_count INTEGER NOT NULL,"
349                                  " client_namespace VARCHAR NOT NULL,"
350                                  " client_id VARCHAR NOT NULL,"
351                                  " online_url VARCHAR NOT NULL,"
352                                  " file_path VARCHAR NOT NULL,"
353                                  " title VARCHAR NOT NULL DEFAULT '',"
354                                  " original_url VARCHAR NOT NULL DEFAULT '',"
355                                  " request_origin VARCHAR NOT NULL DEFAULT ''"
356                                  ")"));
357   ASSERT_TRUE(connection.CommitTransaction());
358   sql::Statement statement(connection.GetUniqueStatement(
359       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
360       "(offline_id, creation_time, file_size, "
361       "last_access_time, access_count, client_namespace, "
362       "client_id, online_url, file_path, title, original_url, "
363       "request_origin) "
364       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
365   statement.BindInt64(0, kOfflineId);
366   statement.BindInt(1, 0);
367   statement.BindInt64(2, kFileSize);
368   statement.BindInt(3, 0);
369   statement.BindInt(4, 1);
370   statement.BindCString(5, kTestClientNamespace);
371   statement.BindString(6, kTestClientId2.id);
372   statement.BindCString(7, kTestURL);
373   statement.BindString(8, base::FilePath(kFilePath).MaybeAsASCII());
374   statement.BindString16(9, base::UTF8ToUTF16("Test title"));
375   statement.BindCString(10, kOriginalTestURL);
376   statement.BindString(11, kTestRequestOrigin);
377   ASSERT_TRUE(statement.Run());
378   ASSERT_TRUE(connection.DoesTableExist(OFFLINE_PAGES_TABLE_V1));
379   ASSERT_FALSE(connection.DoesColumnExist(OFFLINE_PAGES_TABLE_V1, "digest"));
380 }
381 
InjectItemInM62Store(sql::Database * db,const OfflinePageItem & item)382 void InjectItemInM62Store(sql::Database* db, const OfflinePageItem& item) {
383   ASSERT_TRUE(db->BeginTransaction());
384   sql::Statement statement(db->GetUniqueStatement(
385       "INSERT INTO " OFFLINE_PAGES_TABLE_V1
386       "(offline_id, creation_time, file_size, "
387       "last_access_time, access_count, client_namespace, "
388       "client_id, online_url, file_path, title, original_url, "
389       "request_origin, system_download_id, file_missing_time, "
390       "digest) "
391       "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
392   statement.BindInt64(0, item.offline_id);
393   statement.BindInt64(1, store_utils::ToDatabaseTime(item.creation_time));
394   statement.BindInt64(2, item.file_size);
395   statement.BindInt64(3, store_utils::ToDatabaseTime(item.last_access_time));
396   statement.BindInt(4, item.access_count);
397   statement.BindString(5, item.client_id.name_space);
398   statement.BindString(6, item.client_id.id);
399   statement.BindString(7, item.url.spec());
400   statement.BindString(8, store_utils::ToDatabaseFilePath(item.file_path));
401   statement.BindString16(9, item.title);
402   statement.BindString(10, item.original_url_if_different.spec());
403   statement.BindString(11, item.request_origin);
404   statement.BindInt64(12, item.system_download_id);
405   statement.BindInt64(13, store_utils::ToDatabaseTime(item.file_missing_time));
406   statement.BindString(14, item.digest);
407   ASSERT_TRUE(statement.Run());
408   ASSERT_TRUE(db->CommitTransaction());
409 }
410 
BuildTestStoreWithSchemaFromM62(const base::FilePath & file)411 void BuildTestStoreWithSchemaFromM62(const base::FilePath& file) {
412   sql::Database connection;
413   ASSERT_TRUE(
414       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
415   ASSERT_TRUE(connection.is_open());
416   ASSERT_TRUE(connection.BeginTransaction());
417   ASSERT_TRUE(
418       connection.Execute("CREATE TABLE " OFFLINE_PAGES_TABLE_V1
419                          "(offline_id INTEGER PRIMARY KEY NOT NULL,"
420                          " creation_time INTEGER NOT NULL,"
421                          " file_size INTEGER NOT NULL,"
422                          " last_access_time INTEGER NOT NULL,"
423                          " access_count INTEGER NOT NULL,"
424                          " system_download_id INTEGER NOT NULL DEFAULT 0,"
425                          " file_missing_time INTEGER NOT NULL DEFAULT 0,"
426                          " upgrade_attempt INTEGER NOT NULL DEFAULT 0,"
427                          " client_namespace VARCHAR NOT NULL,"
428                          " client_id VARCHAR NOT NULL,"
429                          " online_url VARCHAR NOT NULL,"
430                          " file_path VARCHAR NOT NULL,"
431                          " title VARCHAR NOT NULL DEFAULT '',"
432                          " original_url VARCHAR NOT NULL DEFAULT '',"
433                          " request_origin VARCHAR NOT NULL DEFAULT '',"
434                          " digest VARCHAR NOT NULL DEFAULT ''"
435                          ")"));
436   ASSERT_TRUE(connection.CommitTransaction());
437 
438   OfflinePageItemGenerator generator;
439   generator.SetNamespace(kTestClientNamespace);
440   generator.SetId(kTestClientId2.id);
441   generator.SetUrl(GURL(kTestURL));
442   generator.SetRequestOrigin(kTestRequestOrigin);
443   generator.SetFileSize(kFileSize);
444   OfflinePageItem test_item = generator.CreateItem();
445   test_item.offline_id = kOfflineId;
446   test_item.file_path = base::FilePath(kFilePath);
447   InjectItemInM62Store(&connection, test_item);
448 }
449 
BuildTestStoreWithSchemaVersion1(const base::FilePath & file)450 void BuildTestStoreWithSchemaVersion1(const base::FilePath& file) {
451   BuildTestStoreWithSchemaFromM62(file);
452   sql::Database connection;
453   ASSERT_TRUE(
454       connection.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
455   ASSERT_TRUE(connection.is_open());
456   ASSERT_TRUE(connection.BeginTransaction());
457   sql::MetaTable meta_table;
458   ASSERT_TRUE(meta_table.Init(&connection, 1, 1));
459   ASSERT_TRUE(connection.CommitTransaction());
460 
461   OfflinePageItemGenerator generator;
462   generator.SetUrl(GURL(kTestURL));
463   generator.SetRequestOrigin(kTestRequestOrigin);
464   generator.SetFileSize(kFileSize);
465 
466   generator.SetNamespace(kAsyncNamespace);
467   InjectItemInM62Store(&connection, generator.CreateItem());
468   generator.SetNamespace(kDownloadNamespace);
469   InjectItemInM62Store(&connection, generator.CreateItem());
470   generator.SetNamespace(kBrowserActionsNamespace);
471   InjectItemInM62Store(&connection, generator.CreateItem());
472   generator.SetNamespace(kNTPSuggestionsNamespace);
473   InjectItemInM62Store(&connection, generator.CreateItem());
474 }
475 
BuildTestStoreWithSchemaVersion2(const base::FilePath & file)476 void BuildTestStoreWithSchemaVersion2(const base::FilePath& file) {
477   BuildTestStoreWithSchemaVersion1(file);
478   sql::Database db;
479   ASSERT_TRUE(db.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
480   sql::MetaTable meta_table;
481   ASSERT_TRUE(
482       meta_table.Init(&db, 2, OfflinePageMetadataStore::kCompatibleVersion));
483 }
484 
InsertVisualsVersion3(sql::Database * db,const OfflinePageVisuals & visuals)485 bool InsertVisualsVersion3(sql::Database* db,
486                            const OfflinePageVisuals& visuals) {
487   static const char kInsertVisualsSql[] =
488       "INSERT INTO page_thumbnails"
489       " (offline_id,expiration,thumbnail) VALUES (?,?,?)";
490   sql::Statement statement(
491       db->GetCachedStatement(SQL_FROM_HERE, kInsertVisualsSql));
492   statement.BindInt64(0, visuals.offline_id);
493   statement.BindInt64(1, store_utils::ToDatabaseTime(visuals.expiration));
494   statement.BindBlob(2, visuals.thumbnail.data(), visuals.thumbnail.size());
495   return statement.Run();
496 }
497 
BuildTestStoreWithSchemaVersion3(const base::FilePath & file)498 void BuildTestStoreWithSchemaVersion3(const base::FilePath& file) {
499   BuildTestStoreWithSchemaVersion2(file);
500   sql::Database db;
501   ASSERT_TRUE(db.Open(file.Append(FILE_PATH_LITERAL("OfflinePages.db"))));
502   sql::MetaTable meta_table;
503   ASSERT_TRUE(
504       meta_table.Init(&db, 3, OfflinePageMetadataStore::kCompatibleVersion));
505 
506   static const char kSql[] =
507       "CREATE TABLE page_thumbnails"
508       " (offline_id INTEGER PRIMARY KEY NOT NULL,"
509       " expiration INTEGER NOT NULL,"
510       " thumbnail BLOB NOT NULL"
511       ");";
512   ASSERT_TRUE(db.Execute(kSql));
513   ASSERT_TRUE(InsertVisualsVersion3(&db, TestVisuals()));
514 }
515 
516 // Create an offline page item from a SQL result.  Expects complete rows with
517 // all columns present.
MakeOfflinePageItem(sql::Statement * statement)518 OfflinePageItem MakeOfflinePageItem(sql::Statement* statement) {
519   int64_t id = statement->ColumnInt64(0);
520   base::Time creation_time =
521       store_utils::FromDatabaseTime(statement->ColumnInt64(1));
522   int64_t file_size = statement->ColumnInt64(2);
523   base::Time last_access_time =
524       store_utils::FromDatabaseTime(statement->ColumnInt64(3));
525   int access_count = statement->ColumnInt(4);
526   int64_t system_download_id = statement->ColumnInt64(5);
527   base::Time file_missing_time =
528       store_utils::FromDatabaseTime(statement->ColumnInt64(6));
529   // Column 7 is deprecated 'upgrade_attempt'.
530   ClientId client_id(statement->ColumnString(8), statement->ColumnString(9));
531   GURL url(statement->ColumnString(10));
532   base::FilePath path(
533       store_utils::FromDatabaseFilePath(statement->ColumnString(11)));
534   base::string16 title = statement->ColumnString16(12);
535   GURL original_url(statement->ColumnString(13));
536   std::string request_origin = statement->ColumnString(14);
537   std::string digest = statement->ColumnString(15);
538   std::string snippet = statement->ColumnString(16);
539   std::string attribution = statement->ColumnString(17);
540 
541   OfflinePageItem item(url, id, client_id, path, file_size, creation_time);
542   item.last_access_time = last_access_time;
543   item.access_count = access_count;
544   item.title = title;
545   item.original_url_if_different = original_url;
546   item.request_origin = request_origin;
547   item.system_download_id = system_download_id;
548   item.file_missing_time = file_missing_time;
549   item.digest = digest;
550   item.snippet = snippet;
551   item.attribution = attribution;
552   return item;
553 }
554 
GetOfflinePagesSync(sql::Database * db)555 std::vector<OfflinePageItem> GetOfflinePagesSync(sql::Database* db) {
556   std::vector<OfflinePageItem> result;
557 
558   static const char kSql[] = "SELECT * FROM " OFFLINE_PAGES_TABLE_V1;
559   sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
560 
561   while (statement.Step())
562     result.push_back(MakeOfflinePageItem(&statement));
563 
564   if (!statement.Succeeded()) {
565     result.clear();
566   }
567   return result;
568 }
569 
570 class OfflinePageMetadataStoreTest : public testing::Test {
571  public:
OfflinePageMetadataStoreTest()572   OfflinePageMetadataStoreTest()
573       : task_runner_(new base::TestMockTimeTaskRunner),
574         task_runner_handle_(task_runner_) {
575     EXPECT_TRUE(temp_directory_.CreateUniqueTempDir());
576   }
~OfflinePageMetadataStoreTest()577   ~OfflinePageMetadataStoreTest() override {}
578 
579  protected:
TearDown()580   void TearDown() override {
581     // Wait for all the pieces of the store to delete itself properly.
582     PumpLoop();
583   }
584 
BuildStore()585   std::unique_ptr<OfflinePageMetadataStore> BuildStore() {
586     auto store = std::make_unique<OfflinePageMetadataStore>(
587         base::ThreadTaskRunnerHandle::Get(), TempPath());
588     PumpLoop();
589     return store;
590   }
591 
PumpLoop()592   void PumpLoop() { task_runner_->RunUntilIdle(); }
FastForwardBy(base::TimeDelta delta)593   void FastForwardBy(base::TimeDelta delta) {
594     task_runner_->FastForwardBy(delta);
595   }
596 
task_runner() const597   base::TestMockTimeTaskRunner* task_runner() const {
598     return task_runner_.get();
599   }
TempPath() const600   base::FilePath TempPath() const { return temp_directory_.GetPath(); }
601 
CheckThatStoreHasOneItem(OfflinePageMetadataStore * store)602   OfflinePageItem CheckThatStoreHasOneItem(OfflinePageMetadataStore* store) {
603     std::vector<OfflinePageItem> pages = GetOfflinePages(store);
604     EXPECT_EQ(1U, pages.size());
605     return pages[0];
606   }
607 
CheckThatOfflinePageCanBeSaved(std::unique_ptr<OfflinePageMetadataStore> store)608   void CheckThatOfflinePageCanBeSaved(
609       std::unique_ptr<OfflinePageMetadataStore> store) {
610     size_t store_size = GetOfflinePages(store.get()).size();
611     OfflinePageItem offline_page(GURL(kTestURL), 1234LL, kTestClientId1,
612                                  base::FilePath(kFilePath), kFileSize);
613     offline_page.title = base::UTF8ToUTF16("a title");
614     offline_page.original_url_if_different = GURL(kOriginalTestURL);
615     offline_page.system_download_id = kTestSystemDownloadId;
616     offline_page.digest = kTestDigest;
617     offline_page.snippet = kTestSnippet;
618     offline_page.attribution = kTestAttribution;
619 
620     EXPECT_EQ(ItemActionStatus::SUCCESS,
621               AddOfflinePage(store.get(), offline_page));
622 
623     // Close the store first to ensure file lock is removed.
624     store.reset();
625     store = BuildStore();
626     std::vector<OfflinePageItem> pages = GetOfflinePages(store.get());
627     ASSERT_EQ(store_size + 1, pages.size());
628     if (store_size > 0 && pages[0].offline_id != offline_page.offline_id) {
629       std::swap(pages[0], pages[1]);
630     }
631     EXPECT_EQ(offline_page, pages[0]);
632   }
633 
CheckThatPageVisualsCanBeSaved(OfflinePageMetadataStore * store)634   void CheckThatPageVisualsCanBeSaved(OfflinePageMetadataStore* store) {
635     OfflinePageVisuals visuals;
636     visuals.offline_id = kOfflineId;
637     visuals.expiration = kVisualsExpiration;
638     visuals.thumbnail = "content";
639     visuals.favicon = "favicon";
640 
641     std::vector<OfflinePageVisuals> visuals_vector_before = GetVisuals(store);
642 
643     AddVisuals(store, visuals);
644     std::vector<OfflinePageVisuals> visuals_vector = GetVisuals(store);
645     EXPECT_EQ(visuals_vector_before.size() + 1, visuals_vector.size());
646     EXPECT_EQ(visuals, visuals_vector.back());
647   }
648 
VerifyMetaVersions()649   void VerifyMetaVersions() {
650     sql::Database connection;
651     ASSERT_TRUE(connection.Open(temp_directory_.GetPath().Append(
652         FILE_PATH_LITERAL("OfflinePages.db"))));
653     ASSERT_TRUE(connection.is_open());
654     EXPECT_TRUE(sql::MetaTable::DoesTableExist(&connection));
655     sql::MetaTable meta_table;
656     EXPECT_TRUE(meta_table.Init(&connection, 1, 1));
657 
658     EXPECT_EQ(OfflinePageMetadataStore::kCurrentVersion,
659               meta_table.GetVersionNumber());
660     EXPECT_EQ(OfflinePageMetadataStore::kCompatibleVersion,
661               meta_table.GetCompatibleVersionNumber());
662   }
663 
LoadAndCheckStore()664   void LoadAndCheckStore() {
665     auto store = std::make_unique<OfflinePageMetadataStore>(
666         base::ThreadTaskRunnerHandle::Get(), TempPath());
667     OfflinePageItem item = CheckThatStoreHasOneItem(store.get());
668     CheckThatPageVisualsCanBeSaved(store.get());
669     CheckThatOfflinePageCanBeSaved(std::move(store));
670     VerifyMetaVersions();
671   }
672 
LoadAndCheckStoreFromMetaVersion1AndUp()673   void LoadAndCheckStoreFromMetaVersion1AndUp() {
674     // At meta version 1, more items were added to the database for testing,
675     // which necessitates different checks.
676     auto store = std::make_unique<OfflinePageMetadataStore>(
677         base::ThreadTaskRunnerHandle::Get(), TempPath());
678     std::vector<OfflinePageItem> pages = GetOfflinePages(store.get());
679     EXPECT_EQ(5U, pages.size());
680 
681     CheckThatPageVisualsCanBeSaved((OfflinePageMetadataStore*)store.get());
682     CheckThatOfflinePageCanBeSaved(std::move(store));
683     VerifyMetaVersions();
684   }
685 
LoadAndCheckStoreFromMetaVersion3AndUp()686   void LoadAndCheckStoreFromMetaVersion3AndUp() {
687     auto store = std::make_unique<OfflinePageMetadataStore>(
688         base::ThreadTaskRunnerHandle::Get(), TempPath());
689     std::vector<OfflinePageItem> pages = GetOfflinePages(store.get());
690     EXPECT_EQ(5U, pages.size());
691 
692     std::vector<OfflinePageVisuals> visuals_vector = GetVisuals(store.get());
693     EXPECT_EQ(1U, visuals_vector.size());
694 
695     OfflinePageVisuals visuals_v3 = TestVisuals();
696     visuals_v3.favicon = std::string();
697     EXPECT_EQ(visuals_v3, visuals_vector.back());
698 
699     CheckThatPageVisualsCanBeSaved(store.get());
700     CheckThatOfflinePageCanBeSaved(std::move(store));
701     VerifyMetaVersions();
702   }
703 
704   template <typename T>
ExecuteSync(OfflinePageMetadataStore * store,base::OnceCallback<T (sql::Database *)> run_callback,T default_value)705   T ExecuteSync(OfflinePageMetadataStore* store,
706                 base::OnceCallback<T(sql::Database*)> run_callback,
707                 T default_value) {
708     bool called = false;
709     T result;
710     auto result_callback = base::BindLambdaForTesting([&](T async_result) {
711       result = std::move(async_result);
712       called = true;
713     });
714     store->Execute<T>(std::move(run_callback), result_callback, default_value);
715     PumpLoop();
716     EXPECT_TRUE(called);
717     return result;
718   }
719 
GetOfflinePagesAsync(OfflinePageMetadataStore * store,base::OnceCallback<void (std::vector<OfflinePageItem>)> callback)720   void GetOfflinePagesAsync(
721       OfflinePageMetadataStore* store,
722       base::OnceCallback<void(std::vector<OfflinePageItem>)> callback) {
723     auto run_callback = base::BindOnce(&GetOfflinePagesSync);
724     store->Execute<std::vector<OfflinePageItem>>(std::move(run_callback),
725                                                  std::move(callback), {});
726   }
727 
GetOfflinePages(OfflinePageMetadataStore * store)728   std::vector<OfflinePageItem> GetOfflinePages(
729       OfflinePageMetadataStore* store) {
730     return ExecuteSync<std::vector<OfflinePageItem>>(
731         store, base::BindOnce(&GetOfflinePagesSync), {});
732   }
733 
GetOfflinePageSet(OfflinePageMetadataStore * store)734   OfflinePageSet GetOfflinePageSet(OfflinePageMetadataStore* store) {
735     std::vector<OfflinePageItem> items = GetOfflinePages(store);
736     auto page_set = OfflinePageSet(items.begin(), items.end());
737     CHECK_EQ(page_set.size(), items.size());
738     return page_set;
739   }
740 
AddOfflinePage(OfflinePageMetadataStore * store,const OfflinePageItem & item)741   ItemActionStatus AddOfflinePage(OfflinePageMetadataStore* store,
742                                   const OfflinePageItem& item) {
743     auto result_callback = base::BindLambdaForTesting([&](sql::Database* db) {
744       // Using 'INSERT OR FAIL' or 'INSERT OR ABORT' in the query below
745       // causes debug builds to DLOG.
746       static const char kSql[] =
747           "INSERT OR IGNORE INTO " OFFLINE_PAGES_TABLE_V1
748           " (offline_id,online_url,client_namespace,client_id,"
749           "file_path,"
750           "file_size,creation_time,last_access_time,access_count,"
751           "title,original_url,request_origin,system_download_id,"
752           "file_missing_time,digest,snippet,attribution)"
753           " VALUES "
754           "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)";
755 
756       sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
757       statement.BindInt64(0, item.offline_id);
758       statement.BindString(1, item.url.spec());
759       statement.BindString(2, item.client_id.name_space);
760       statement.BindString(3, item.client_id.id);
761       statement.BindString(4, store_utils::ToDatabaseFilePath(item.file_path));
762       statement.BindInt64(5, item.file_size);
763       statement.BindInt64(6, store_utils::ToDatabaseTime(item.creation_time));
764       statement.BindInt64(7,
765                           store_utils::ToDatabaseTime(item.last_access_time));
766       statement.BindInt(8, item.access_count);
767       statement.BindString16(9, item.title);
768       statement.BindString(10, item.original_url_if_different.spec());
769       statement.BindString(11, item.request_origin);
770       statement.BindInt64(12, item.system_download_id);
771       statement.BindInt64(13,
772                           store_utils::ToDatabaseTime(item.file_missing_time));
773       statement.BindString(14, item.digest);
774       statement.BindString(15, item.snippet);
775       statement.BindString(16, item.attribution);
776 
777       if (!statement.Run())
778         return ItemActionStatus::STORE_ERROR;
779       if (db->GetLastChangeCount() == 0)
780         return ItemActionStatus::ALREADY_EXISTS;
781       return ItemActionStatus::SUCCESS;
782     });
783     return ExecuteSync<ItemActionStatus>(store, result_callback,
784                                          ItemActionStatus::SUCCESS);
785   }
786 
GetVisuals(OfflinePageMetadataStore * store)787   std::vector<OfflinePageVisuals> GetVisuals(OfflinePageMetadataStore* store) {
788     std::vector<OfflinePageVisuals> visuals_vector;
789     auto run_callback = base::BindLambdaForTesting([&](sql::Database* db) {
790       static const char kSql[] = "SELECT * FROM page_thumbnails";
791       sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
792 
793       while (statement.Step()) {
794         OfflinePageVisuals visuals;
795         visuals.offline_id = statement.ColumnInt64(0);
796         visuals.expiration =
797             store_utils::FromDatabaseTime(statement.ColumnInt64(1));
798         statement.ColumnBlobAsString(2, &visuals.thumbnail);
799         statement.ColumnBlobAsString(3, &visuals.favicon);
800         visuals_vector.push_back(std::move(visuals));
801       }
802 
803       EXPECT_TRUE(statement.Succeeded());
804       return visuals_vector;
805     });
806     return ExecuteSync<std::vector<OfflinePageVisuals>>(store, run_callback,
807                                                         {});
808   }
809 
AddVisuals(OfflinePageMetadataStore * store,const OfflinePageVisuals & visuals)810   void AddVisuals(OfflinePageMetadataStore* store,
811                   const OfflinePageVisuals& visuals) {
812     std::vector<OfflinePageVisuals> visuals_vector;
813     auto run_callback = base::BindLambdaForTesting([&](sql::Database* db) {
814       static const char kSql[] =
815           "INSERT INTO page_thumbnails"
816           " (offline_id,expiration,thumbnail,favicon) VALUES (?,?,?,?)";
817       sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE, kSql));
818 
819       statement.BindInt64(0, visuals.offline_id);
820       statement.BindInt64(1, store_utils::ToDatabaseTime(visuals.expiration));
821       statement.BindString(2, visuals.thumbnail);
822       statement.BindString(3, visuals.favicon);
823       EXPECT_TRUE(statement.Run());
824       return visuals_vector;
825     });
826     ExecuteSync<std::vector<OfflinePageVisuals>>(store, run_callback, {});
827   }
828 
829  protected:
830   base::ScopedTempDir temp_directory_;
831   scoped_refptr<base::TestMockTimeTaskRunner> task_runner_;
832   base::ThreadTaskRunnerHandle task_runner_handle_;
833 };
834 
835 // Loads empty store and makes sure that there are no offline pages stored in
836 // it.
TEST_F(OfflinePageMetadataStoreTest,LoadEmptyStore)837 TEST_F(OfflinePageMetadataStoreTest, LoadEmptyStore) {
838   std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
839   EXPECT_EQ(0U, GetOfflinePages(store.get()).size());
840 }
841 
TEST_F(OfflinePageMetadataStoreTest,GetOfflinePagesFromInvalidStore)842 TEST_F(OfflinePageMetadataStoreTest, GetOfflinePagesFromInvalidStore) {
843   std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
844 
845   // Because execute method is self-healing this part of the test expects a
846   // positive results now.
847   store->SetInitializationStatusForTesting(
848       InitializationStatus::kNotInitialized, false);
849   EXPECT_EQ(OfflinePageSet(), GetOfflinePageSet(store.get()));
850   EXPECT_EQ(InitializationStatus::kSuccess,
851             store->initialization_status_for_testing());
852 
853   store->SetInitializationStatusForTesting(InitializationStatus::kFailure,
854                                            false);
855   EXPECT_EQ(OfflinePageSet(), GetOfflinePageSet(store.get()));
856   EXPECT_EQ(InitializationStatus::kFailure,
857             store->initialization_status_for_testing());
858 
859   store->SetInitializationStatusForTesting(InitializationStatus::kSuccess,
860                                            true);
861   EXPECT_EQ(OfflinePageSet(), GetOfflinePageSet(store.get()));
862 
863   store->SetInitializationStatusForTesting(
864       InitializationStatus::kNotInitialized, true);
865   EXPECT_EQ(OfflinePageSet(), GetOfflinePageSet(store.get()));
866 
867   store->SetInitializationStatusForTesting(InitializationStatus::kFailure,
868                                            false);
869   EXPECT_EQ(OfflinePageSet(), GetOfflinePageSet(store.get()));
870 }
871 
872 // Loads a store which has an outdated schema.
873 // These tests would crash if it's not handling correctly when we're loading
874 // old version stores.
TEST_F(OfflinePageMetadataStoreTest,LoadVersion52Store)875 TEST_F(OfflinePageMetadataStoreTest, LoadVersion52Store) {
876   BuildTestStoreWithSchemaFromM52(TempPath());
877   LoadAndCheckStore();
878 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion53Store)879 TEST_F(OfflinePageMetadataStoreTest, LoadVersion53Store) {
880   BuildTestStoreWithSchemaFromM53(TempPath());
881   LoadAndCheckStore();
882 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion54Store)883 TEST_F(OfflinePageMetadataStoreTest, LoadVersion54Store) {
884   BuildTestStoreWithSchemaFromM54(TempPath());
885   LoadAndCheckStore();
886 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion55Store)887 TEST_F(OfflinePageMetadataStoreTest, LoadVersion55Store) {
888   BuildTestStoreWithSchemaFromM55(TempPath());
889   LoadAndCheckStore();
890 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion56Store)891 TEST_F(OfflinePageMetadataStoreTest, LoadVersion56Store) {
892   BuildTestStoreWithSchemaFromM56(TempPath());
893   LoadAndCheckStore();
894 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion57Store)895 TEST_F(OfflinePageMetadataStoreTest, LoadVersion57Store) {
896   BuildTestStoreWithSchemaFromM57(TempPath());
897   LoadAndCheckStore();
898 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion61Store)899 TEST_F(OfflinePageMetadataStoreTest, LoadVersion61Store) {
900   BuildTestStoreWithSchemaFromM61(TempPath());
901   LoadAndCheckStore();
902 }
TEST_F(OfflinePageMetadataStoreTest,LoadVersion62Store)903 TEST_F(OfflinePageMetadataStoreTest, LoadVersion62Store) {
904   BuildTestStoreWithSchemaFromM62(TempPath());
905   LoadAndCheckStore();
906 }
907 
TEST_F(OfflinePageMetadataStoreTest,LoadStoreWithMetaVersion1)908 TEST_F(OfflinePageMetadataStoreTest, LoadStoreWithMetaVersion1) {
909   BuildTestStoreWithSchemaVersion1(TempPath());
910   LoadAndCheckStoreFromMetaVersion1AndUp();
911 }
912 
TEST_F(OfflinePageMetadataStoreTest,LoadStoreWithMetaVersion2)913 TEST_F(OfflinePageMetadataStoreTest, LoadStoreWithMetaVersion2) {
914   BuildTestStoreWithSchemaVersion2(TempPath());
915   LoadAndCheckStoreFromMetaVersion1AndUp();
916 }
917 
TEST_F(OfflinePageMetadataStoreTest,LoadStoreWithMetaVersion3)918 TEST_F(OfflinePageMetadataStoreTest, LoadStoreWithMetaVersion3) {
919   BuildTestStoreWithSchemaVersion3(TempPath());
920   LoadAndCheckStoreFromMetaVersion3AndUp();
921 }
922 
923 // Adds metadata of an offline page into a store and then opens the store
924 // again to make sure that stored metadata survives store restarts.
TEST_F(OfflinePageMetadataStoreTest,AddOfflinePage)925 TEST_F(OfflinePageMetadataStoreTest, AddOfflinePage) {
926   CheckThatOfflinePageCanBeSaved(BuildStore());
927 }
928 
TEST_F(OfflinePageMetadataStoreTest,AddSameOfflinePageTwice)929 TEST_F(OfflinePageMetadataStoreTest, AddSameOfflinePageTwice) {
930   std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
931 
932   OfflinePageItem offline_page(GURL(kTestURL), 1234LL, kTestClientId1,
933                                base::FilePath(kFilePath), kFileSize);
934   offline_page.title = base::UTF8ToUTF16("a title");
935 
936   EXPECT_EQ(ItemActionStatus::SUCCESS,
937             AddOfflinePage(store.get(), offline_page));
938 
939   EXPECT_EQ(ItemActionStatus::ALREADY_EXISTS,
940             AddOfflinePage(store.get(), offline_page));
941 }
942 
943 // Adds metadata of multiple offline pages into a store and removes some.
TEST_F(OfflinePageMetadataStoreTest,AddRemoveMultipleOfflinePages)944 TEST_F(OfflinePageMetadataStoreTest, AddRemoveMultipleOfflinePages) {
945   std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
946 
947   // Add an offline page.
948   OfflinePageItem offline_page_1(GURL(kTestURL), 12345LL, kTestClientId1,
949                                  base::FilePath(kFilePath), kFileSize);
950   EXPECT_EQ(ItemActionStatus::SUCCESS,
951             AddOfflinePage(store.get(), offline_page_1));
952 
953   // Add anther offline page.
954   base::FilePath file_path_2 =
955       base::FilePath(FILE_PATH_LITERAL("//other.page.com.mhtml"));
956   OfflinePageItem offline_page_2(GURL("https://other.page.com"), 5678LL,
957                                  kTestClientId2, file_path_2, 12345,
958                                  OfflineTimeNow());
959   offline_page_2.request_origin = kTestRequestOrigin;
960   offline_page_2.original_url_if_different = GURL("https://example.com/bar");
961   offline_page_2.system_download_id = kTestSystemDownloadId;
962   offline_page_2.digest = kTestDigest;
963 
964   EXPECT_EQ(ItemActionStatus::SUCCESS,
965             AddOfflinePage(store.get(), offline_page_2));
966 
967   // Check all pages are in the store.
968   EXPECT_EQ(OfflinePageSet({offline_page_1, offline_page_2}),
969             GetOfflinePageSet(store.get()));
970 
971   // Close and reload the store.
972   store.reset();
973   store = BuildStore();
974   EXPECT_EQ(OfflinePageSet({offline_page_1, offline_page_2}),
975             GetOfflinePageSet(store.get()));
976 }
977 
TEST_F(OfflinePageMetadataStoreTest,StoreCloses)978 TEST_F(OfflinePageMetadataStoreTest, StoreCloses) {
979   std::unique_ptr<OfflinePageMetadataStore> store(BuildStore());
980   GetOfflinePages(store.get());
981 
982   EXPECT_TRUE(task_runner()->HasPendingTask());
983   EXPECT_LT(base::TimeDelta(), task_runner()->NextPendingTaskDelay());
984 
985   FastForwardBy(OfflinePageMetadataStore::kClosingDelay);
986   PumpLoop();
987   EXPECT_EQ(StoreState::NOT_LOADED, store->GetStateForTesting());
988 
989   // Ensure that next call to the store will actually reinitialize it.
990   EXPECT_EQ(0U, GetOfflinePages(store.get()).size());
991   EXPECT_EQ(StoreState::LOADED, store->GetStateForTesting());
992 }
993 
TEST_F(OfflinePageMetadataStoreTest,MultiplePendingCalls)994 TEST_F(OfflinePageMetadataStoreTest, MultiplePendingCalls) {
995   auto store = std::make_unique<OfflinePageMetadataStore>(
996       base::ThreadTaskRunnerHandle::Get(), TempPath());
997   EXPECT_FALSE(task_runner()->HasPendingTask());
998   EXPECT_EQ(StoreState::NOT_LOADED, store->GetStateForTesting());
999 
1000   // First call flips the state to initializing.
1001   // Subsequent calls should be pending until store is initialized.
1002   int callback_count = 0;
1003   auto get_complete =
1004       base::BindLambdaForTesting([&](std::vector<OfflinePageItem> pages) {
1005         ++callback_count;
1006         EXPECT_TRUE(pages.empty());
1007       });
1008   GetOfflinePagesAsync(store.get(), get_complete);
1009   EXPECT_EQ(StoreState::INITIALIZING, store->GetStateForTesting());
1010 
1011   GetOfflinePagesAsync(store.get(), get_complete);
1012   EXPECT_EQ(0U, GetOfflinePages(store.get()).size());
1013   EXPECT_EQ(StoreState::LOADED, store->GetStateForTesting());
1014   EXPECT_EQ(2, callback_count);
1015 }
1016 
1017 }  // namespace
1018 }  // namespace offline_pages
1019