summaryrefslogtreecommitdiffstats
path: root/browser/components/sessionstore/test/browser_backup_recovery.js
blob: 0610c3cf83ec7b515ff54dc90cb801cb6a3dab56 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

// This tests are for a sessionstore.js atomic backup.
// Each test will wait for a write to the Session Store
// before executing.

const PREF_SS_INTERVAL = "browser.sessionstore.interval";
const Paths = SessionFile.Paths;

// Global variables that contain sessionstore.jsonlz4 and sessionstore.baklz4 data for
// comparison between tests.
var gSSData;
var gSSBakData;

function promiseRead(path) {
  return IOUtils.readUTF8(path, { decompress: true });
}

async function reInitSessionFile() {
  await SessionFile.wipe();
  await SessionFile.read();
}

add_setup(async function () {
  // Make sure that we are not racing with SessionSaver's time based
  // saves.
  Services.prefs.setIntPref(PREF_SS_INTERVAL, 10000000);
  registerCleanupFunction(() => Services.prefs.clearUserPref(PREF_SS_INTERVAL));
});

add_task(async function test_creation() {
  // Cancel all pending session saves so they won't get in our way.
  SessionSaver.cancel();

  // Create dummy sessionstore backups
  let OLD_BACKUP = PathUtils.join(PathUtils.profileDir, "sessionstore.baklz4");
  let OLD_UPGRADE_BACKUP = PathUtils.join(
    PathUtils.profileDir,
    "sessionstore.baklz4-0000000"
  );

  await IOUtils.writeUTF8(OLD_BACKUP, "sessionstore.bak");
  await IOUtils.writeUTF8(OLD_UPGRADE_BACKUP, "sessionstore upgrade backup");

  await reInitSessionFile();

  // Ensure none of the sessionstore files and backups exists
  for (let k of Paths.loadOrder) {
    ok(
      !(await IOUtils.exists(Paths[k])),
      "After wipe " + k + " sessionstore file doesn't exist"
    );
  }
  ok(
    !(await IOUtils.exists(OLD_BACKUP)),
    "After wipe, old backup doesn't exist"
  );
  ok(
    !(await IOUtils.exists(OLD_UPGRADE_BACKUP)),
    "After wipe, old upgrade backup doesn't exist"
  );

  // Open a new tab, save session, ensure that the correct files exist.
  let URL_BASE =
    "http://example.com/?atomic_backup_test_creation=" + Math.random();
  let URL = URL_BASE + "?first_write";
  let tab = BrowserTestUtils.addTab(gBrowser, URL);

  info("Testing situation after a single write");
  await promiseBrowserLoaded(tab.linkedBrowser);
  await TabStateFlusher.flush(tab.linkedBrowser);
  await SessionSaver.run();

  ok(
    await IOUtils.exists(Paths.recovery),
    "After write, recovery sessionstore file exists again"
  );
  ok(
    !(await IOUtils.exists(Paths.recoveryBackup)),
    "After write, recoveryBackup sessionstore doesn't exist"
  );
  ok(
    (await promiseRead(Paths.recovery)).includes(URL),
    "Recovery sessionstore file contains the required tab"
  );
  ok(
    !(await IOUtils.exists(Paths.clean)),
    "After first write, clean shutdown " +
      "sessionstore doesn't exist, since we haven't shutdown yet"
  );

  // Open a second tab, save session, ensure that the correct files exist.
  info("Testing situation after a second write");
  let URL2 = URL_BASE + "?second_write";
  BrowserTestUtils.loadURIString(tab.linkedBrowser, URL2);
  await promiseBrowserLoaded(tab.linkedBrowser);
  await TabStateFlusher.flush(tab.linkedBrowser);
  await SessionSaver.run();

  ok(
    await IOUtils.exists(Paths.recovery),
    "After second write, recovery sessionstore file still exists"
  );
  ok(
    (await promiseRead(Paths.recovery)).includes(URL2),
    "Recovery sessionstore file contains the latest url"
  );
  ok(
    await IOUtils.exists(Paths.recoveryBackup),
    "After write, recoveryBackup sessionstore now exists"
  );
  let backup = await promiseRead(Paths.recoveryBackup);
  ok(!backup.includes(URL2), "Recovery backup doesn't contain the latest url");
  ok(backup.includes(URL), "Recovery backup contains the original url");
  ok(
    !(await IOUtils.exists(Paths.clean)),
    "After first write, clean shutdown " +
      "sessionstore doesn't exist, since we haven't shutdown yet"
  );

  info("Reinitialize, ensure that we haven't leaked sensitive files");
  await SessionFile.read(); // Reinitializes SessionFile
  await SessionSaver.run();
  ok(
    !(await IOUtils.exists(Paths.clean)),
    "After second write, clean shutdown " +
      "sessionstore doesn't exist, since we haven't shutdown yet"
  );
  ok(
    Paths.upgradeBackup === "",
    "After second write, clean " +
      "shutdown sessionstore doesn't exist, since we haven't shutdown yet"
  );
  ok(
    !(await IOUtils.exists(Paths.nextUpgradeBackup)),
    "After second write, clean " +
      "shutdown sessionstore doesn't exist, since we haven't shutdown yet"
  );

  gBrowser.removeTab(tab);
});

var promiseSource = async function (name) {
  let URL =
    "http://example.com/?atomic_backup_test_recovery=" +
    Math.random() +
    "&name=" +
    name;
  let tab = BrowserTestUtils.addTab(gBrowser, URL);

  await promiseBrowserLoaded(tab.linkedBrowser);
  await TabStateFlusher.flush(tab.linkedBrowser);
  await SessionSaver.run();
  gBrowser.removeTab(tab);

  let SOURCE = await promiseRead(Paths.recovery);
  await SessionFile.wipe();
  return SOURCE;
};

add_task(async function test_recovery() {
  await reInitSessionFile();
  info("Attempting to recover from the recovery file");

  // Create Paths.recovery, ensure that we can recover from it.
  let SOURCE = await promiseSource("Paths.recovery");
  await IOUtils.makeDirectory(Paths.backups);
  await IOUtils.writeUTF8(Paths.recovery, SOURCE, { compress: true });
  is(
    (await SessionFile.read()).source,
    SOURCE,
    "Recovered the correct source from the recovery file"
  );

  info("Corrupting recovery file, attempting to recover from recovery backup");
  SOURCE = await promiseSource("Paths.recoveryBackup");
  await IOUtils.makeDirectory(Paths.backups);
  await IOUtils.writeUTF8(Paths.recoveryBackup, SOURCE, { compress: true });
  await IOUtils.writeUTF8(Paths.recovery, "<Invalid JSON>", { compress: true });
  is(
    (await SessionFile.read()).source,
    SOURCE,
    "Recovered the correct source from the recovery file"
  );
});

add_task(async function test_recovery_inaccessible() {
  // Can't do chmod() on non-UNIX platforms, we need that for this test.
  if (AppConstants.platform != "macosx" && AppConstants.platform != "linux") {
    return;
  }

  await reInitSessionFile();
  info(
    "Making recovery file inaccessible, attempting to recover from recovery backup"
  );
  let SOURCE_RECOVERY = await promiseSource("Paths.recovery");
  let SOURCE = await promiseSource("Paths.recoveryBackup");
  await IOUtils.makeDirectory(Paths.backups);
  await IOUtils.writeUTF8(Paths.recoveryBackup, SOURCE, { compress: true });

  // Write a valid recovery file but make it inaccessible.
  await IOUtils.writeUTF8(Paths.recovery, SOURCE_RECOVERY, { compress: true });
  await IOUtils.setPermissions(Paths.recovery, 0);

  is(
    (await SessionFile.read()).source,
    SOURCE,
    "Recovered the correct source from the recovery file"
  );
  await IOUtils.setPermissions(Paths.recovery, 0o644);
});

add_task(async function test_clean() {
  await reInitSessionFile();
  let SOURCE = await promiseSource("Paths.clean");
  await IOUtils.writeUTF8(Paths.clean, SOURCE, { compress: true });
  await SessionFile.read();
  await SessionSaver.run();
  is(
    await promiseRead(Paths.cleanBackup),
    SOURCE,
    "After first read/write, " +
      "clean shutdown file has been moved to cleanBackup"
  );
});

/**
 * Tests loading of sessionstore when format version is known.
 */
add_task(async function test_version() {
  info("Preparing sessionstore");
  let SOURCE = await promiseSource("Paths.clean");

  // Check there's a format version number
  is(
    JSON.parse(SOURCE).version[0],
    "sessionrestore",
    "Found sessionstore format version"
  );

  // Create Paths.clean file
  await IOUtils.makeDirectory(Paths.backups);
  await IOUtils.writeUTF8(Paths.clean, SOURCE, { compress: true });

  info("Attempting to recover from the clean file");
  // Ensure that we can recover from Paths.recovery
  is(
    (await SessionFile.read()).source,
    SOURCE,
    "Recovered the correct source from the clean file"
  );
});

/**
 * Tests fallback to previous backups if format version is unknown.
 */
add_task(async function test_version_fallback() {
  await reInitSessionFile();
  info("Preparing data, making sure that it has a version number");
  let SOURCE = await promiseSource("Paths.clean");
  let BACKUP_SOURCE = await promiseSource("Paths.cleanBackup");

  is(
    JSON.parse(SOURCE).version[0],
    "sessionrestore",
    "Found sessionstore format version"
  );
  is(
    JSON.parse(BACKUP_SOURCE).version[0],
    "sessionrestore",
    "Found backup sessionstore format version"
  );

  await IOUtils.makeDirectory(Paths.backups);

  info(
    "Modifying format version number to something incorrect, to make sure that we disregard the file."
  );
  let parsedSource = JSON.parse(SOURCE);
  parsedSource.version[0] = "bookmarks";
  await IOUtils.writeJSON(Paths.clean, parsedSource, { compress: true });
  await IOUtils.writeUTF8(Paths.cleanBackup, BACKUP_SOURCE, { compress: true });
  is(
    (await SessionFile.read()).source,
    BACKUP_SOURCE,
    "Recovered the correct source from the backup recovery file"
  );

  info(
    "Modifying format version number to a future version, to make sure that we disregard the file."
  );
  parsedSource = JSON.parse(SOURCE);
  parsedSource.version[1] = Number.MAX_SAFE_INTEGER;
  await IOUtils.writeJSON(Paths.clean, parsedSource, { compress: true });
  await IOUtils.writeUTF8(Paths.cleanBackup, BACKUP_SOURCE, { compress: true });
  is(
    (await SessionFile.read()).source,
    BACKUP_SOURCE,
    "Recovered the correct source from the backup recovery file"
  );
});

add_task(async function cleanup() {
  await reInitSessionFile();
});