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 "base/bind.h"
6#include "base/files/file_util.h"
7#include "base/files/scoped_temp_dir.h"
8#include "base/message_loop/message_loop.h"
9#include "base/message_loop/message_loop_proxy.h"
10#include "base/strings/utf_string_conversions.h"
11#include "base/threading/sequenced_worker_pool.h"
12#include "base/time/time.h"
13#include "content/browser/dom_storage/dom_storage_area.h"
14#include "content/browser/dom_storage/dom_storage_database.h"
15#include "content/browser/dom_storage/dom_storage_database_adapter.h"
16#include "content/browser/dom_storage/dom_storage_task_runner.h"
17#include "content/browser/dom_storage/local_storage_database_adapter.h"
18#include "content/common/dom_storage/dom_storage_types.h"
19#include "testing/gtest/include/gtest/gtest.h"
20
21using base::ASCIIToUTF16;
22
23namespace content {
24
25class DOMStorageAreaTest : public testing::Test {
26 public:
27  DOMStorageAreaTest()
28    : kOrigin(GURL("http://dom_storage/")),
29      kKey(ASCIIToUTF16("key")),
30      kValue(ASCIIToUTF16("value")),
31      kKey2(ASCIIToUTF16("key2")),
32      kValue2(ASCIIToUTF16("value2")) {
33  }
34
35  const GURL kOrigin;
36  const base::string16 kKey;
37  const base::string16 kValue;
38  const base::string16 kKey2;
39  const base::string16 kValue2;
40
41  // Method used in the CommitTasks test case.
42  void InjectedCommitSequencingTask(DOMStorageArea* area) {
43    // At this point the OnCommitTimer has run.
44    // Verify that it put a commit in flight.
45    EXPECT_EQ(1, area->commit_batches_in_flight_);
46    EXPECT_FALSE(area->commit_batch_.get());
47    EXPECT_TRUE(area->HasUncommittedChanges());
48    // Make additional change and verify that a new commit batch
49    // is created for that change.
50    base::NullableString16 old_value;
51    EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_value));
52    EXPECT_TRUE(area->commit_batch_.get());
53    EXPECT_EQ(1, area->commit_batches_in_flight_);
54    EXPECT_TRUE(area->HasUncommittedChanges());
55  }
56
57  // Class used in the CommitChangesAtShutdown test case.
58  class VerifyChangesCommittedDatabase : public DOMStorageDatabase {
59   public:
60    VerifyChangesCommittedDatabase() {}
61    virtual ~VerifyChangesCommittedDatabase() {
62      const base::string16 kKey(ASCIIToUTF16("key"));
63      const base::string16 kValue(ASCIIToUTF16("value"));
64      DOMStorageValuesMap values;
65      ReadAllValues(&values);
66      EXPECT_EQ(1u, values.size());
67      EXPECT_EQ(kValue, values[kKey].string());
68    }
69  };
70
71 private:
72  base::MessageLoop message_loop_;
73};
74
75TEST_F(DOMStorageAreaTest, DOMStorageAreaBasics) {
76  scoped_refptr<DOMStorageArea> area(
77      new DOMStorageArea(1, std::string(), kOrigin, NULL, NULL));
78  base::string16 old_value;
79  base::NullableString16 old_nullable_value;
80  scoped_refptr<DOMStorageArea> copy;
81
82  // We don't focus on the underlying DOMStorageMap functionality
83  // since that's covered by seperate unit tests.
84  EXPECT_EQ(kOrigin, area->origin());
85  EXPECT_EQ(1, area->namespace_id());
86  EXPECT_EQ(0u, area->Length());
87  EXPECT_TRUE(area->SetItem(kKey, kValue, &old_nullable_value));
88  EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_nullable_value));
89  EXPECT_FALSE(area->HasUncommittedChanges());
90
91  // Verify that a copy shares the same map.
92  copy = area->ShallowCopy(2, std::string());
93  EXPECT_EQ(kOrigin, copy->origin());
94  EXPECT_EQ(2, copy->namespace_id());
95  EXPECT_EQ(area->Length(), copy->Length());
96  EXPECT_EQ(area->GetItem(kKey).string(), copy->GetItem(kKey).string());
97  EXPECT_EQ(area->Key(0).string(), copy->Key(0).string());
98  EXPECT_EQ(copy->map_.get(), area->map_.get());
99
100  // But will deep copy-on-write as needed.
101  EXPECT_TRUE(area->RemoveItem(kKey, &old_value));
102  EXPECT_NE(copy->map_.get(), area->map_.get());
103  copy = area->ShallowCopy(2, std::string());
104  EXPECT_EQ(copy->map_.get(), area->map_.get());
105  EXPECT_TRUE(area->SetItem(kKey, kValue, &old_nullable_value));
106  EXPECT_NE(copy->map_.get(), area->map_.get());
107  copy = area->ShallowCopy(2, std::string());
108  EXPECT_EQ(copy->map_.get(), area->map_.get());
109  EXPECT_NE(0u, area->Length());
110  EXPECT_TRUE(area->Clear());
111  EXPECT_EQ(0u, area->Length());
112  EXPECT_NE(copy->map_.get(), area->map_.get());
113
114  // Verify that once Shutdown(), behaves that way.
115  area->Shutdown();
116  EXPECT_TRUE(area->is_shutdown_);
117  EXPECT_FALSE(area->map_.get());
118  EXPECT_EQ(0u, area->Length());
119  EXPECT_TRUE(area->Key(0).is_null());
120  EXPECT_TRUE(area->GetItem(kKey).is_null());
121  EXPECT_FALSE(area->SetItem(kKey, kValue, &old_nullable_value));
122  EXPECT_FALSE(area->RemoveItem(kKey, &old_value));
123  EXPECT_FALSE(area->Clear());
124}
125
126TEST_F(DOMStorageAreaTest, BackingDatabaseOpened) {
127  const int64 kSessionStorageNamespaceId = kLocalStorageNamespaceId + 1;
128  base::ScopedTempDir temp_dir;
129  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
130  const base::FilePath kExpectedOriginFilePath = temp_dir.path().Append(
131      DOMStorageArea::DatabaseFileNameFromOrigin(kOrigin));
132
133  // No directory, backing should be null.
134  {
135    scoped_refptr<DOMStorageArea> area(
136        new DOMStorageArea(kOrigin, base::FilePath(), NULL));
137    EXPECT_EQ(NULL, area->backing_.get());
138    EXPECT_TRUE(area->is_initial_import_done_);
139    EXPECT_FALSE(base::PathExists(kExpectedOriginFilePath));
140  }
141
142  // Valid directory and origin but no session storage backing. Backing should
143  // be null.
144  {
145    scoped_refptr<DOMStorageArea> area(
146        new DOMStorageArea(kSessionStorageNamespaceId, std::string(), kOrigin,
147                           NULL, NULL));
148    EXPECT_EQ(NULL, area->backing_.get());
149    EXPECT_TRUE(area->is_initial_import_done_);
150
151    base::NullableString16 old_value;
152    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
153    ASSERT_TRUE(old_value.is_null());
154
155    // Check that saving a value has still left us without a backing database.
156    EXPECT_EQ(NULL, area->backing_.get());
157    EXPECT_FALSE(base::PathExists(kExpectedOriginFilePath));
158  }
159
160  // This should set up a DOMStorageArea that is correctly backed to disk.
161  {
162    scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
163        kOrigin,
164        temp_dir.path(),
165        new MockDOMStorageTaskRunner(base::MessageLoopProxy::current().get())));
166
167    EXPECT_TRUE(area->backing_.get());
168    DOMStorageDatabase* database = static_cast<LocalStorageDatabaseAdapter*>(
169        area->backing_.get())->db_.get();
170    EXPECT_FALSE(database->IsOpen());
171    EXPECT_FALSE(area->is_initial_import_done_);
172
173    // Inject an in-memory db to speed up the test.
174    // We will verify that something is written into the database but not
175    // that a file is written to disk - DOMStorageDatabase unit tests cover
176    // that.
177    area->backing_.reset(new LocalStorageDatabaseAdapter());
178
179    // Need to write something to ensure that the database is created.
180    base::NullableString16 old_value;
181    EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
182    ASSERT_TRUE(old_value.is_null());
183    EXPECT_TRUE(area->is_initial_import_done_);
184    EXPECT_TRUE(area->commit_batch_.get());
185    EXPECT_EQ(0, area->commit_batches_in_flight_);
186
187    base::MessageLoop::current()->RunUntilIdle();
188
189    EXPECT_FALSE(area->commit_batch_.get());
190    EXPECT_EQ(0, area->commit_batches_in_flight_);
191    database = static_cast<LocalStorageDatabaseAdapter*>(
192        area->backing_.get())->db_.get();
193    EXPECT_TRUE(database->IsOpen());
194    EXPECT_EQ(1u, area->Length());
195    EXPECT_EQ(kValue, area->GetItem(kKey).string());
196
197    // Verify the content made it to the in memory database.
198    DOMStorageValuesMap values;
199    area->backing_->ReadAllValues(&values);
200    EXPECT_EQ(1u, values.size());
201    EXPECT_EQ(kValue, values[kKey].string());
202  }
203}
204
205TEST_F(DOMStorageAreaTest, CommitTasks) {
206  base::ScopedTempDir temp_dir;
207  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
208
209  scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
210      kOrigin,
211      temp_dir.path(),
212      new MockDOMStorageTaskRunner(base::MessageLoopProxy::current().get())));
213  // Inject an in-memory db to speed up the test.
214  area->backing_.reset(new LocalStorageDatabaseAdapter());
215
216  // Unrelated to commits, but while we're here, see that querying Length()
217  // causes the backing database to be opened and presumably read from.
218  EXPECT_FALSE(area->is_initial_import_done_);
219  EXPECT_EQ(0u, area->Length());
220  EXPECT_TRUE(area->is_initial_import_done_);
221
222  DOMStorageValuesMap values;
223  base::NullableString16 old_value;
224
225  // See that changes are batched up.
226  EXPECT_FALSE(area->commit_batch_.get());
227  EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
228  EXPECT_TRUE(area->HasUncommittedChanges());
229  EXPECT_TRUE(area->commit_batch_.get());
230  EXPECT_FALSE(area->commit_batch_->clear_all_first);
231  EXPECT_EQ(1u, area->commit_batch_->changed_values.size());
232  EXPECT_TRUE(area->SetItem(kKey2, kValue2, &old_value));
233  EXPECT_TRUE(area->commit_batch_.get());
234  EXPECT_FALSE(area->commit_batch_->clear_all_first);
235  EXPECT_EQ(2u, area->commit_batch_->changed_values.size());
236  base::MessageLoop::current()->RunUntilIdle();
237  EXPECT_FALSE(area->HasUncommittedChanges());
238  EXPECT_FALSE(area->commit_batch_.get());
239  EXPECT_EQ(0, area->commit_batches_in_flight_);
240  // Verify the changes made it to the database.
241  values.clear();
242  area->backing_->ReadAllValues(&values);
243  EXPECT_EQ(2u, values.size());
244  EXPECT_EQ(kValue, values[kKey].string());
245  EXPECT_EQ(kValue2, values[kKey2].string());
246
247  // See that clear is handled properly.
248  EXPECT_TRUE(area->Clear());
249  EXPECT_TRUE(area->commit_batch_.get());
250  EXPECT_TRUE(area->commit_batch_->clear_all_first);
251  EXPECT_TRUE(area->commit_batch_->changed_values.empty());
252  base::MessageLoop::current()->RunUntilIdle();
253  EXPECT_FALSE(area->commit_batch_.get());
254  EXPECT_EQ(0, area->commit_batches_in_flight_);
255  // Verify the changes made it to the database.
256  values.clear();
257  area->backing_->ReadAllValues(&values);
258  EXPECT_TRUE(values.empty());
259
260  // See that if changes accrue while a commit is "in flight"
261  // those will also get committed.
262  EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
263  EXPECT_TRUE(area->HasUncommittedChanges());
264  // At this point the OnCommitTimer task has been posted. We inject
265  // another task in the queue that will execute after the timer task,
266  // but before the CommitChanges task. From within our injected task,
267  // we'll make an additional SetItem() call.
268  base::MessageLoop::current()->PostTask(
269      FROM_HERE,
270      base::Bind(&DOMStorageAreaTest::InjectedCommitSequencingTask,
271                 base::Unretained(this),
272                 area));
273  base::MessageLoop::current()->RunUntilIdle();
274  EXPECT_TRUE(area->HasOneRef());
275  EXPECT_FALSE(area->HasUncommittedChanges());
276  // Verify the changes made it to the database.
277  values.clear();
278  area->backing_->ReadAllValues(&values);
279  EXPECT_EQ(2u, values.size());
280  EXPECT_EQ(kValue, values[kKey].string());
281  EXPECT_EQ(kValue2, values[kKey2].string());
282}
283
284TEST_F(DOMStorageAreaTest, CommitChangesAtShutdown) {
285  base::ScopedTempDir temp_dir;
286  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
287  scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
288      kOrigin,
289      temp_dir.path(),
290      new MockDOMStorageTaskRunner(base::MessageLoopProxy::current().get())));
291
292  // Inject an in-memory db to speed up the test and also to verify
293  // the final changes are commited in it's dtor.
294  static_cast<LocalStorageDatabaseAdapter*>(area->backing_.get())->db_.reset(
295      new VerifyChangesCommittedDatabase());
296
297  DOMStorageValuesMap values;
298  base::NullableString16 old_value;
299  EXPECT_TRUE(area->SetItem(kKey, kValue, &old_value));
300  EXPECT_TRUE(area->HasUncommittedChanges());
301  area->backing_->ReadAllValues(&values);
302  EXPECT_TRUE(values.empty());  // not committed yet
303  area->Shutdown();
304  base::MessageLoop::current()->RunUntilIdle();
305  EXPECT_TRUE(area->HasOneRef());
306  EXPECT_FALSE(area->backing_.get());
307  // The VerifyChangesCommittedDatabase destructor verifies values
308  // were committed.
309}
310
311TEST_F(DOMStorageAreaTest, DeleteOrigin) {
312  base::ScopedTempDir temp_dir;
313  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
314  scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
315      kOrigin,
316      temp_dir.path(),
317      new MockDOMStorageTaskRunner(base::MessageLoopProxy::current().get())));
318
319  // This test puts files on disk.
320  base::FilePath db_file_path = static_cast<LocalStorageDatabaseAdapter*>(
321      area->backing_.get())->db_->file_path();
322  base::FilePath db_journal_file_path =
323      DOMStorageDatabase::GetJournalFilePath(db_file_path);
324
325  // Nothing bad should happen when invoked w/o any files on disk.
326  area->DeleteOrigin();
327  EXPECT_FALSE(base::PathExists(db_file_path));
328
329  // Commit something in the database and then delete.
330  base::NullableString16 old_value;
331  area->SetItem(kKey, kValue, &old_value);
332  base::MessageLoop::current()->RunUntilIdle();
333  EXPECT_TRUE(base::PathExists(db_file_path));
334  area->DeleteOrigin();
335  EXPECT_EQ(0u, area->Length());
336  EXPECT_FALSE(base::PathExists(db_file_path));
337  EXPECT_FALSE(base::PathExists(db_journal_file_path));
338
339  // Put some uncommitted changes to a non-existing database in
340  // and then delete. No file ever gets created in this case.
341  area->SetItem(kKey, kValue, &old_value);
342  EXPECT_TRUE(area->HasUncommittedChanges());
343  EXPECT_EQ(1u, area->Length());
344  area->DeleteOrigin();
345  EXPECT_TRUE(area->HasUncommittedChanges());
346  EXPECT_EQ(0u, area->Length());
347  base::MessageLoop::current()->RunUntilIdle();
348  EXPECT_FALSE(area->HasUncommittedChanges());
349  EXPECT_FALSE(base::PathExists(db_file_path));
350
351  // Put some uncommitted changes to a an existing database in
352  // and then delete.
353  area->SetItem(kKey, kValue, &old_value);
354  base::MessageLoop::current()->RunUntilIdle();
355  EXPECT_TRUE(base::PathExists(db_file_path));
356  area->SetItem(kKey2, kValue2, &old_value);
357  EXPECT_TRUE(area->HasUncommittedChanges());
358  EXPECT_EQ(2u, area->Length());
359  area->DeleteOrigin();
360  EXPECT_TRUE(area->HasUncommittedChanges());
361  EXPECT_EQ(0u, area->Length());
362  base::MessageLoop::current()->RunUntilIdle();
363  EXPECT_FALSE(area->HasUncommittedChanges());
364  // Since the area had uncommitted changes at the time delete
365  // was called, the file will linger until the shutdown time.
366  EXPECT_TRUE(base::PathExists(db_file_path));
367  area->Shutdown();
368  base::MessageLoop::current()->RunUntilIdle();
369  EXPECT_FALSE(base::PathExists(db_file_path));
370}
371
372TEST_F(DOMStorageAreaTest, PurgeMemory) {
373  base::ScopedTempDir temp_dir;
374  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
375  scoped_refptr<DOMStorageArea> area(new DOMStorageArea(
376      kOrigin,
377      temp_dir.path(),
378      new MockDOMStorageTaskRunner(base::MessageLoopProxy::current().get())));
379
380  // Inject an in-memory db to speed up the test.
381  area->backing_.reset(new LocalStorageDatabaseAdapter());
382
383  // Unowned ptrs we use to verify that 'purge' has happened.
384  DOMStorageDatabase* original_backing =
385      static_cast<LocalStorageDatabaseAdapter*>(
386          area->backing_.get())->db_.get();
387  DOMStorageMap* original_map = area->map_.get();
388
389  // Should do no harm when called on a newly constructed object.
390  EXPECT_FALSE(area->is_initial_import_done_);
391  area->PurgeMemory();
392  EXPECT_FALSE(area->is_initial_import_done_);
393  DOMStorageDatabase* new_backing = static_cast<LocalStorageDatabaseAdapter*>(
394      area->backing_.get())->db_.get();
395  EXPECT_EQ(original_backing, new_backing);
396  EXPECT_EQ(original_map, area->map_.get());
397
398  // Should not do anything when commits are pending.
399  base::NullableString16 old_value;
400  area->SetItem(kKey, kValue, &old_value);
401  EXPECT_TRUE(area->is_initial_import_done_);
402  EXPECT_TRUE(area->HasUncommittedChanges());
403  area->PurgeMemory();
404  EXPECT_TRUE(area->is_initial_import_done_);
405  EXPECT_TRUE(area->HasUncommittedChanges());
406  new_backing = static_cast<LocalStorageDatabaseAdapter*>(
407      area->backing_.get())->db_.get();
408  EXPECT_EQ(original_backing, new_backing);
409  EXPECT_EQ(original_map, area->map_.get());
410
411  // Commit the changes from above,
412  base::MessageLoop::current()->RunUntilIdle();
413  EXPECT_FALSE(area->HasUncommittedChanges());
414  new_backing = static_cast<LocalStorageDatabaseAdapter*>(
415      area->backing_.get())->db_.get();
416  EXPECT_EQ(original_backing, new_backing);
417  EXPECT_EQ(original_map, area->map_.get());
418
419  // Should drop caches and reset database connections
420  // when invoked on an area that's loaded up primed.
421  area->PurgeMemory();
422  EXPECT_FALSE(area->is_initial_import_done_);
423  new_backing = static_cast<LocalStorageDatabaseAdapter*>(
424      area->backing_.get())->db_.get();
425  EXPECT_NE(original_backing, new_backing);
426  EXPECT_NE(original_map, area->map_.get());
427}
428
429TEST_F(DOMStorageAreaTest, DatabaseFileNames) {
430  struct {
431    const char* origin;
432    const char* file_name;
433    const char* journal_file_name;
434  } kCases[] = {
435    { "https://www.google.com/",
436      "https_www.google.com_0.localstorage",
437      "https_www.google.com_0.localstorage-journal" },
438    { "http://www.google.com:8080/",
439      "http_www.google.com_8080.localstorage",
440      "http_www.google.com_8080.localstorage-journal" },
441    { "file:///",
442      "file__0.localstorage",
443      "file__0.localstorage-journal" },
444  };
445
446  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kCases); ++i) {
447    GURL origin = GURL(kCases[i].origin).GetOrigin();
448    base::FilePath file_name =
449        base::FilePath().AppendASCII(kCases[i].file_name);
450    base::FilePath journal_file_name =
451        base::FilePath().AppendASCII(kCases[i].journal_file_name);
452
453    EXPECT_EQ(file_name,
454              DOMStorageArea::DatabaseFileNameFromOrigin(origin));
455    EXPECT_EQ(origin,
456              DOMStorageArea::OriginFromDatabaseFileName(file_name));
457    EXPECT_EQ(journal_file_name,
458              DOMStorageDatabase::GetJournalFilePath(file_name));
459  }
460
461  // Also test some DOMStorageDatabase::GetJournalFilePath cases here.
462  base::FilePath parent = base::FilePath().AppendASCII("a").AppendASCII("b");
463  EXPECT_EQ(
464      parent.AppendASCII("file-journal"),
465      DOMStorageDatabase::GetJournalFilePath(parent.AppendASCII("file")));
466  EXPECT_EQ(
467      base::FilePath().AppendASCII("-journal"),
468      DOMStorageDatabase::GetJournalFilePath(base::FilePath()));
469  EXPECT_EQ(
470      base::FilePath().AppendASCII(".extensiononly-journal"),
471      DOMStorageDatabase::GetJournalFilePath(
472          base::FilePath().AppendASCII(".extensiononly")));
473}
474
475}  // namespace content
476