summaryrefslogtreecommitdiffstats
path: root/toolkit/components/passwordmgr/test/unit/test_loginsBackup.js
blob: 775f6f5486ce0e5ce4d42d7abd1dc647977d8990 (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
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * Tests if logins-backup.json is used correctly in the event that logins.json is missing or corrupt.
 */

"use strict";

ChromeUtils.defineESModuleGetters(this, {
  LoginStore: "resource://gre/modules/LoginStore.sys.mjs",
});
const { TestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TestUtils.sys.mjs"
);
const { TelemetryTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/TelemetryTestUtils.sys.mjs"
);

const rawLogin1 = {
  id: 1,
  hostname: "http://www.example.com",
  httpRealm: null,
  formSubmitURL: "http://www.example.com",
  usernameField: "field_" + String.fromCharCode(533, 537, 7570, 345),
  passwordField: "field_" + String.fromCharCode(421, 259, 349, 537),
  encryptedUsername: "(test)",
  encryptedPassword: "(test)",
  guid: "(test)",
  encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
  timeCreated: Date.now(),
  timeLastUsed: Date.now(),
  timePasswordChanged: Date.now(),
  timesUsed: 1,
};

const rawLogin2 = {
  id: 2,
  hostname: "http://www.example2.com",
  httpRealm: null,
  formSubmitURL: "http://www.example2.com",
  usernameField: "field_2" + String.fromCharCode(533, 537, 7570, 345),
  passwordField: "field_2" + String.fromCharCode(421, 259, 349, 537),
  encryptedUsername: "(test2)",
  encryptedPassword: "(test2)",
  guid: "(test2)",
  encType: Ci.nsILoginManagerCrypto.ENCTYPE_SDR,
  timeCreated: Date.now(),
  timeLastUsed: Date.now(),
  timePasswordChanged: Date.now(),
  timesUsed: 1,
};

// Enable the collection (during test) for all products so even products
// that don't collect the data will be able to run the test without failure.
Services.prefs.setBoolPref(
  "toolkit.telemetry.testing.overrideProductsCheck",
  true
);

/**
 * Tests that logins-backup.json can be used by JSONFile.load() when logins.json is missing or cannot be read.
 */
add_task(async function test_logins_store_missing_or_corrupt_with_backup() {
  const loginsStorePath = PathUtils.join(PathUtils.profileDir, "logins.json");
  const loginsStoreBackup = PathUtils.join(
    PathUtils.profileDir,
    "logins-backup.json"
  );

  // Get store.data ready.
  let store = new LoginStore(loginsStorePath, loginsStoreBackup);
  await store.load();

  // Files should not exist at start up.
  Assert.ok(!(await IOUtils.exists(store.path)), "No store file at start up");
  Assert.ok(
    !(await IOUtils.exists(store._options.backupTo)),
    "No backup file at start up"
  );

  // Add logins to create logins.json and logins-backup.json.
  store.data.logins.push(rawLogin1);
  await store._save();
  Assert.ok(await IOUtils.exists(store.path));

  store.data.logins.push(rawLogin2);
  await store._save();
  Assert.ok(await IOUtils.exists(store._options.backupTo));

  // Remove logins.json and see if logins-backup.json will be used.
  await IOUtils.remove(store.path);
  store.data.logins = [];
  store.dataReady = false;
  Assert.ok(!(await IOUtils.exists(store.path)));
  Assert.ok(await IOUtils.exists(store._options.backupTo));

  // Clear any telemetry events recorded in the jsonfile category previously.
  Services.telemetry.clearEvents();

  await store.load();

  Assert.ok(
    await IOUtils.exists(store.path),
    "logins.json is restored as expected after it went missing"
  );

  Assert.ok(await IOUtils.exists(store._options.backupTo));
  Assert.equal(
    store.data.logins.length,
    1,
    "Logins backup was used successfully when logins.json was missing"
  );

  TelemetryTestUtils.assertEvents(
    [
      ["jsonfile", "load", "logins"],
      ["jsonfile", "load", "logins", "used_backup"],
    ],
    {},
    { clear: true }
  );
  info(
    "Telemetry was recorded accurately when logins-backup.json is used when logins.json was missing"
  );

  // Corrupt the logins.json file.
  let string = '{"logins":[{"hostname":"http://www.example.com","id":1,';
  await IOUtils.writeUTF8(store.path, string, {
    tmpPath: `${store.path}.tmp`,
  });

  // Clear events recorded in the jsonfile category previously.
  Services.telemetry.clearEvents();

  // Try to load the corrupt file.
  store.data.logins = [];
  store.dataReady = false;
  await store.load();

  Assert.ok(
    await IOUtils.exists(`${store.path}.corrupt`),
    "logins.json.corrupt created"
  );
  Assert.ok(
    await IOUtils.exists(store.path),
    "logins.json is restored after it was corrupted"
  );

  // Data should be loaded from logins-backup.json.
  Assert.ok(await IOUtils.exists(store._options.backupTo));
  Assert.equal(
    store.data.logins.length,
    1,
    "Logins backup was used successfully when logins.json was corrupt"
  );

  TelemetryTestUtils.assertEvents(
    [
      ["jsonfile", "load", "logins", ""],
      ["jsonfile", "load", "logins", "invalid_json"],
      ["jsonfile", "load", "logins", "used_backup"],
    ],
    {},
    { clear: true }
  );
  info(
    "Telemetry was recorded accurately when logins-backup.json is used when logins.json was corrupt"
  );

  // Clean up before we start the second part of the test.
  await IOUtils.remove(`${store.path}.corrupt`);

  // Test that the backup file can be used by JSONFile.ensureDataReady() correctly when logins.json is missing.
  // Remove logins.json
  await IOUtils.remove(store.path);
  store.data.logins = [];
  store.dataReady = false;
  Assert.ok(!(await IOUtils.exists(store.path)));
  Assert.ok(await IOUtils.exists(store._options.backupTo));

  store.ensureDataReady();

  // Important to check here if logins.json is restored as expected
  // after it went missing.
  await IOUtils.exists(store.path);

  Assert.ok(await IOUtils.exists(store._options.backupTo));
  await TestUtils.waitForCondition(() => {
    return store.data.logins.length == 1;
  });

  // Test that the backup file is used by JSONFile.ensureDataReady() when logins.json is corrupt.
  // Corrupt the logins.json file.
  await IOUtils.writeUTF8(store.path, string, {
    tmpPath: `${store.path}.tmp`,
  });

  // Try to load the corrupt file.
  store.data.logins = [];
  store.dataReady = false;
  store.ensureDataReady();

  Assert.ok(
    await IOUtils.exists(`${store.path}.corrupt`),
    "logins.json.corrupt created"
  );
  Assert.ok(
    await IOUtils.exists(store.path),
    "logins.json is restored after it was corrupted"
  );

  // Data should be loaded from logins-backup.json.
  Assert.ok(await IOUtils.exists(store._options.backupTo));
  await TestUtils.waitForCondition(() => {
    return store.data.logins.length == 1;
  });
});