1// Copyright 2013 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 "content/browser/dom_storage/dom_storage_database.h"
6
7#include "base/file_util.h"
8#include "base/files/file_path.h"
9#include "base/files/scoped_temp_dir.h"
10#include "base/path_service.h"
11#include "base/strings/utf_string_conversions.h"
12#include "sql/statement.h"
13#include "sql/test/scoped_error_ignorer.h"
14#include "testing/gtest/include/gtest/gtest.h"
15#include "third_party/sqlite/sqlite3.h"
16
17namespace content {
18
19void CreateV1Table(sql::Connection* db) {
20  ASSERT_TRUE(db->is_open());
21  ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable"));
22  ASSERT_TRUE(db->Execute(
23      "CREATE TABLE ItemTable ("
24      "key TEXT UNIQUE ON CONFLICT REPLACE, "
25      "value TEXT NOT NULL ON CONFLICT FAIL)"));
26}
27
28void CreateV2Table(sql::Connection* db) {
29  ASSERT_TRUE(db->is_open());
30  ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable"));
31  ASSERT_TRUE(db->Execute(
32      "CREATE TABLE ItemTable ("
33      "key TEXT UNIQUE ON CONFLICT REPLACE, "
34      "value BLOB NOT NULL ON CONFLICT FAIL)"));
35}
36
37void CreateInvalidKeyColumnTable(sql::Connection* db) {
38  // Create a table with the key type as FLOAT - this is "invalid"
39  // as far as the DOM Storage db is concerned.
40  ASSERT_TRUE(db->is_open());
41  ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable"));
42  ASSERT_TRUE(db->Execute(
43      "CREATE TABLE IF NOT EXISTS ItemTable ("
44      "key FLOAT UNIQUE ON CONFLICT REPLACE, "
45      "value BLOB NOT NULL ON CONFLICT FAIL)"));
46}
47void CreateInvalidValueColumnTable(sql::Connection* db) {
48  // Create a table with the value type as FLOAT - this is "invalid"
49  // as far as the DOM Storage db is concerned.
50  ASSERT_TRUE(db->is_open());
51  ASSERT_TRUE(db->Execute("DROP TABLE IF EXISTS ItemTable"));
52  ASSERT_TRUE(db->Execute(
53      "CREATE TABLE IF NOT EXISTS ItemTable ("
54      "key TEXT UNIQUE ON CONFLICT REPLACE, "
55      "value FLOAT NOT NULL ON CONFLICT FAIL)"));
56}
57
58void InsertDataV1(sql::Connection* db,
59                  const base::string16& key,
60                  const base::string16& value) {
61  sql::Statement statement(db->GetCachedStatement(SQL_FROM_HERE,
62      "INSERT INTO ItemTable VALUES (?,?)"));
63  statement.BindString16(0, key);
64  statement.BindString16(1, value);
65  ASSERT_TRUE(statement.is_valid());
66  statement.Run();
67}
68
69void CheckValuesMatch(DOMStorageDatabase* db,
70                      const DOMStorageValuesMap& expected) {
71  DOMStorageValuesMap values_read;
72  db->ReadAllValues(&values_read);
73  EXPECT_EQ(expected.size(), values_read.size());
74
75  DOMStorageValuesMap::const_iterator it = values_read.begin();
76  for (; it != values_read.end(); ++it) {
77    base::string16 key = it->first;
78    base::NullableString16 value = it->second;
79    base::NullableString16 expected_value = expected.find(key)->second;
80    EXPECT_EQ(expected_value.string(), value.string());
81    EXPECT_EQ(expected_value.is_null(), value.is_null());
82  }
83}
84
85void CreateMapWithValues(DOMStorageValuesMap* values) {
86  base::string16 kCannedKeys[] = {
87      ASCIIToUTF16("test"),
88      ASCIIToUTF16("company"),
89      ASCIIToUTF16("date"),
90      ASCIIToUTF16("empty")
91  };
92  base::NullableString16 kCannedValues[] = {
93      base::NullableString16(ASCIIToUTF16("123"), false),
94      base::NullableString16(ASCIIToUTF16("Google"), false),
95      base::NullableString16(ASCIIToUTF16("18-01-2012"), false),
96      base::NullableString16(base::string16(), false)
97  };
98  for (unsigned i = 0; i < sizeof(kCannedKeys) / sizeof(kCannedKeys[0]); i++)
99    (*values)[kCannedKeys[i]] = kCannedValues[i];
100}
101
102TEST(DOMStorageDatabaseTest, SimpleOpenAndClose) {
103  DOMStorageDatabase db;
104  EXPECT_FALSE(db.IsOpen());
105  ASSERT_TRUE(db.LazyOpen(true));
106  EXPECT_TRUE(db.IsOpen());
107  EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion());
108  db.Close();
109  EXPECT_FALSE(db.IsOpen());
110}
111
112TEST(DOMStorageDatabaseTest, CloseEmptyDatabaseDeletesFile) {
113  base::ScopedTempDir temp_dir;
114  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
115  base::FilePath file_name =
116      temp_dir.path().AppendASCII("TestDOMStorageDatabase.db");
117  DOMStorageValuesMap storage;
118  CreateMapWithValues(&storage);
119
120  // First test the case that explicitly clearing the database will
121  // trigger its deletion from disk.
122  {
123    DOMStorageDatabase db(file_name);
124    EXPECT_EQ(file_name, db.file_path());
125    ASSERT_TRUE(db.CommitChanges(false, storage));
126  }
127  EXPECT_TRUE(base::PathExists(file_name));
128
129  {
130    // Check that reading an existing db with data in it
131    // keeps the DB on disk on close.
132    DOMStorageDatabase db(file_name);
133    DOMStorageValuesMap values;
134    db.ReadAllValues(&values);
135    EXPECT_EQ(storage.size(), values.size());
136  }
137
138  EXPECT_TRUE(base::PathExists(file_name));
139  storage.clear();
140
141  {
142    DOMStorageDatabase db(file_name);
143    ASSERT_TRUE(db.CommitChanges(true, storage));
144  }
145  EXPECT_FALSE(base::PathExists(file_name));
146
147  // Now ensure that a series of updates and removals whose net effect
148  // is an empty database also triggers deletion.
149  CreateMapWithValues(&storage);
150  {
151    DOMStorageDatabase db(file_name);
152    ASSERT_TRUE(db.CommitChanges(false, storage));
153  }
154
155  EXPECT_TRUE(base::PathExists(file_name));
156
157  {
158    DOMStorageDatabase db(file_name);
159    ASSERT_TRUE(db.CommitChanges(false, storage));
160    DOMStorageValuesMap::iterator it = storage.begin();
161    for (; it != storage.end(); ++it)
162      it->second = base::NullableString16();
163    ASSERT_TRUE(db.CommitChanges(false, storage));
164  }
165  EXPECT_FALSE(base::PathExists(file_name));
166}
167
168TEST(DOMStorageDatabaseTest, TestLazyOpenIsLazy) {
169  // This test needs to operate with a file on disk to ensure that we will
170  // open a file that already exists when only invoking ReadAllValues.
171  base::ScopedTempDir temp_dir;
172  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
173  base::FilePath file_name =
174      temp_dir.path().AppendASCII("TestDOMStorageDatabase.db");
175
176  DOMStorageDatabase db(file_name);
177  EXPECT_FALSE(db.IsOpen());
178  DOMStorageValuesMap values;
179  db.ReadAllValues(&values);
180  // Reading an empty db should not open the database.
181  EXPECT_FALSE(db.IsOpen());
182
183  values[ASCIIToUTF16("key")] =
184      base::NullableString16(ASCIIToUTF16("value"), false);
185  db.CommitChanges(false, values);
186  // Writing content should open the database.
187  EXPECT_TRUE(db.IsOpen());
188
189  db.Close();
190  ASSERT_FALSE(db.IsOpen());
191
192  // Reading from an existing database should open the database.
193  CheckValuesMatch(&db, values);
194  EXPECT_TRUE(db.IsOpen());
195}
196
197TEST(DOMStorageDatabaseTest, TestDetectSchemaVersion) {
198  DOMStorageDatabase db;
199  db.db_.reset(new sql::Connection());
200  ASSERT_TRUE(db.db_->OpenInMemory());
201
202  CreateInvalidValueColumnTable(db.db_.get());
203  EXPECT_EQ(DOMStorageDatabase::INVALID, db.DetectSchemaVersion());
204
205  CreateInvalidKeyColumnTable(db.db_.get());
206  EXPECT_EQ(DOMStorageDatabase::INVALID, db.DetectSchemaVersion());
207
208  CreateV1Table(db.db_.get());
209  EXPECT_EQ(DOMStorageDatabase::V1, db.DetectSchemaVersion());
210
211  CreateV2Table(db.db_.get());
212  EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion());
213}
214
215TEST(DOMStorageDatabaseTest, TestLazyOpenUpgradesDatabase) {
216  // This test needs to operate with a file on disk so that we
217  // can create a table at version 1 and then close it again
218  // so that LazyOpen sees there is work to do (LazyOpen will return
219  // early if the database is already open).
220  base::ScopedTempDir temp_dir;
221  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
222  base::FilePath file_name =
223      temp_dir.path().AppendASCII("TestDOMStorageDatabase.db");
224
225  DOMStorageDatabase db(file_name);
226  db.db_.reset(new sql::Connection());
227  ASSERT_TRUE(db.db_->Open(file_name));
228  CreateV1Table(db.db_.get());
229  db.Close();
230
231  EXPECT_TRUE(db.LazyOpen(true));
232  EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion());
233}
234
235TEST(DOMStorageDatabaseTest, SimpleWriteAndReadBack) {
236  DOMStorageDatabase db;
237
238  DOMStorageValuesMap storage;
239  CreateMapWithValues(&storage);
240
241  EXPECT_TRUE(db.CommitChanges(false, storage));
242  CheckValuesMatch(&db, storage);
243}
244
245TEST(DOMStorageDatabaseTest, WriteWithClear) {
246  DOMStorageDatabase db;
247
248  DOMStorageValuesMap storage;
249  CreateMapWithValues(&storage);
250
251  ASSERT_TRUE(db.CommitChanges(false, storage));
252  CheckValuesMatch(&db, storage);
253
254  // Insert some values, clearing the database first.
255  storage.clear();
256  storage[ASCIIToUTF16("another_key")] =
257      base::NullableString16(ASCIIToUTF16("test"), false);
258  ASSERT_TRUE(db.CommitChanges(true, storage));
259  CheckValuesMatch(&db, storage);
260
261  // Now clear the values without inserting any new ones.
262  storage.clear();
263  ASSERT_TRUE(db.CommitChanges(true, storage));
264  CheckValuesMatch(&db, storage);
265}
266
267TEST(DOMStorageDatabaseTest, UpgradeFromV1ToV2WithData) {
268  const base::string16 kCannedKey = ASCIIToUTF16("foo");
269  const base::NullableString16 kCannedValue(ASCIIToUTF16("bar"), false);
270  DOMStorageValuesMap expected;
271  expected[kCannedKey] = kCannedValue;
272
273  DOMStorageDatabase db;
274  db.db_.reset(new sql::Connection());
275  ASSERT_TRUE(db.db_->OpenInMemory());
276  CreateV1Table(db.db_.get());
277  InsertDataV1(db.db_.get(), kCannedKey, kCannedValue.string());
278
279  ASSERT_TRUE(db.UpgradeVersion1To2());
280
281  EXPECT_EQ(DOMStorageDatabase::V2, db.DetectSchemaVersion());
282
283  CheckValuesMatch(&db, expected);
284}
285
286TEST(DOMStorageDatabaseTest, TestSimpleRemoveOneValue) {
287  DOMStorageDatabase db;
288
289  ASSERT_TRUE(db.LazyOpen(true));
290  const base::string16 kCannedKey = ASCIIToUTF16("test");
291  const base::NullableString16 kCannedValue(ASCIIToUTF16("data"), false);
292  DOMStorageValuesMap expected;
293  expected[kCannedKey] = kCannedValue;
294
295  // First write some data into the database.
296  ASSERT_TRUE(db.CommitChanges(false, expected));
297  CheckValuesMatch(&db, expected);
298
299  DOMStorageValuesMap values;
300  // A null string in the map should mean that that key gets
301  // removed.
302  values[kCannedKey] = base::NullableString16();
303  EXPECT_TRUE(db.CommitChanges(false, values));
304
305  expected.clear();
306  CheckValuesMatch(&db, expected);
307}
308
309TEST(DOMStorageDatabaseTest, TestCanOpenAndReadWebCoreDatabase) {
310  base::FilePath webcore_database;
311  PathService::Get(base::DIR_SOURCE_ROOT, &webcore_database);
312  webcore_database = webcore_database.AppendASCII("webkit");
313  webcore_database = webcore_database.AppendASCII("data");
314  webcore_database = webcore_database.AppendASCII("dom_storage");
315  webcore_database =
316      webcore_database.AppendASCII("webcore_test_database.localstorage");
317
318  ASSERT_TRUE(base::PathExists(webcore_database));
319
320  DOMStorageDatabase db(webcore_database);
321  DOMStorageValuesMap values;
322  db.ReadAllValues(&values);
323  EXPECT_TRUE(db.IsOpen());
324  EXPECT_EQ(2u, values.size());
325
326  DOMStorageValuesMap::const_iterator it =
327      values.find(ASCIIToUTF16("value"));
328  EXPECT_TRUE(it != values.end());
329  EXPECT_EQ(ASCIIToUTF16("I am in local storage!"), it->second.string());
330
331  it = values.find(ASCIIToUTF16("timestamp"));
332  EXPECT_TRUE(it != values.end());
333  EXPECT_EQ(ASCIIToUTF16("1326738338841"), it->second.string());
334
335  it = values.find(ASCIIToUTF16("not_there"));
336  EXPECT_TRUE(it == values.end());
337}
338
339TEST(DOMStorageDatabaseTest, TestCanOpenFileThatIsNotADatabase) {
340  // Write into the temporary file first.
341  base::ScopedTempDir temp_dir;
342  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
343  base::FilePath file_name =
344      temp_dir.path().AppendASCII("TestDOMStorageDatabase.db");
345
346  const char kData[] = "I am not a database.";
347  file_util::WriteFile(file_name, kData, strlen(kData));
348
349  {
350    sql::ScopedErrorIgnorer ignore_errors;
351    ignore_errors.IgnoreError(SQLITE_IOERR_SHORT_READ);
352
353    // Try and open the file. As it's not a database, we should end up deleting
354    // it and creating a new, valid file, so everything should actually
355    // succeed.
356    DOMStorageDatabase db(file_name);
357    DOMStorageValuesMap values;
358    CreateMapWithValues(&values);
359    EXPECT_TRUE(db.CommitChanges(true, values));
360    EXPECT_TRUE(db.CommitChanges(false, values));
361    EXPECT_TRUE(db.IsOpen());
362
363    CheckValuesMatch(&db, values);
364
365    ASSERT_TRUE(ignore_errors.CheckIgnoredErrors());
366  }
367
368  {
369    sql::ScopedErrorIgnorer ignore_errors;
370    ignore_errors.IgnoreError(SQLITE_CANTOPEN);
371
372    // Try to open a directory, we should fail gracefully and not attempt
373    // to delete it.
374    DOMStorageDatabase db(temp_dir.path());
375    DOMStorageValuesMap values;
376    CreateMapWithValues(&values);
377    EXPECT_FALSE(db.CommitChanges(true, values));
378    EXPECT_FALSE(db.CommitChanges(false, values));
379    EXPECT_FALSE(db.IsOpen());
380
381    values.clear();
382
383    db.ReadAllValues(&values);
384    EXPECT_EQ(0u, values.size());
385    EXPECT_FALSE(db.IsOpen());
386
387    EXPECT_TRUE(base::PathExists(temp_dir.path()));
388
389    ASSERT_TRUE(ignore_errors.CheckIgnoredErrors());
390  }
391}
392
393}  // namespace content
394