summaryrefslogtreecommitdiffstats
path: root/services/settings/test/unit/test_remote_settings_recover_broken.js
blob: c5f82d69492cc1e3c374425a3c862add8e67d667 (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
/* import-globals-from ../../../common/tests/unit/head_helpers.js */

const { SyncHistory } = ChromeUtils.importESModule(
  "resource://services-settings/SyncHistory.sys.mjs"
);
const { RemoteSettingsClient } = ChromeUtils.importESModule(
  "resource://services-settings/RemoteSettingsClient.sys.mjs"
);
const { RemoteSettings } = ChromeUtils.importESModule(
  "resource://services-settings/remote-settings.sys.mjs"
);
const { Utils } = ChromeUtils.importESModule(
  "resource://services-settings/Utils.sys.mjs"
);

const PREF_SETTINGS_SERVER = "services.settings.server";
const CHANGES_PATH = "/v1" + Utils.CHANGES_PATH;
const BROKEN_SYNC_THRESHOLD = 10; // See default pref value

let server;
let client;
let maybeSyncBackup;

async function clear_state() {
  // Disable logging output.
  Services.prefs.setStringPref("services.settings.loglevel", "critical");
  // Pull data from the test server.
  Services.prefs.setStringPref(
    PREF_SETTINGS_SERVER,
    `http://localhost:${server.identity.primaryPort}/v1`
  );

  // Clear sync history.
  await new SyncHistory("").clear();

  // Simulate a response whose ETag gets incremented on each call
  // (in order to generate several history entries, indexed by timestamp).
  let timestamp = 1337;
  server.registerPathHandler(CHANGES_PATH, (request, response) => {
    response.setStatusLine(null, 200, "OK");
    response.setHeader("Content-Type", "application/json; charset=UTF-8");
    response.setHeader("Date", new Date(1000000).toUTCString());
    response.setHeader("ETag", `"${timestamp}"`);
    response.write(
      JSON.stringify({
        timestamp,
        changes: [
          {
            last_modified: ++timestamp,
            bucket: "main",
            collection: "desktop-manager",
          },
        ],
      })
    );
  });

  // Restore original maybeSync() method between each test.
  client.maybeSync = maybeSyncBackup;
}

function run_test() {
  // Set up an HTTP Server
  server = new HttpServer();
  server.start(-1);

  client = RemoteSettings("desktop-manager");
  maybeSyncBackup = client.maybeSync;

  run_next_test();

  registerCleanupFunction(() => {
    server.stop(() => {});
    // Restore original maybeSync() method when test suite is done.
    client.maybeSync = maybeSyncBackup;
  });
}

add_task(clear_state);

add_task(async function test_db_is_destroyed_when_sync_is_broken() {
  // Simulate a successful sync.
  client.maybeSync = async () => {
    // Store some data in local DB.
    await client.db.importChanges({}, 1515, []);
  };
  await RemoteSettings.pollChanges({ trigger: "timer" });

  // Register a client with a failing sync method.
  client.maybeSync = () => {
    throw new RemoteSettingsClient.InvalidSignatureError(
      "main/desktop-manager"
    );
  };

  // Now obtain several failures in a row.
  for (var i = 0; i < BROKEN_SYNC_THRESHOLD; i++) {
    try {
      await RemoteSettings.pollChanges({ trigger: "timer" });
    } catch (e) {}
  }

  // Synchronization is in broken state.
  Assert.equal(
    await client.db.getLastModified(),
    1515,
    "Local DB was not destroyed yet"
  );

  // Synchronize again. Broken state will be detected.
  try {
    await RemoteSettings.pollChanges({ trigger: "timer" });
  } catch (e) {}

  // DB was destroyed.
  Assert.equal(
    await client.db.getLastModified(),
    null,
    "Local DB was destroyed"
  );
});

add_task(clear_state);

add_task(async function test_db_is_not_destroyed_when_state_is_server_error() {
  // Since we don't mock the server endpoints to obtain the changeset of this
  // collection, the call to `maybeSync()` will fail with network errors.

  // Store some data in local DB.
  await client.db.importChanges({}, 1515, []);

  // Now obtain several failures in a row.
  let lastError;
  for (var i = 0; i < BROKEN_SYNC_THRESHOLD + 1; i++) {
    try {
      await RemoteSettings.pollChanges({ trigger: "timer" });
    } catch (e) {
      lastError = e;
    }
  }
  Assert.ok(
    /Cannot parse server content/.test(lastError.message),
    "Error is about server"
  );
  // DB was not destroyed.
  Assert.equal(
    await client.db.getLastModified(),
    1515,
    "Local DB was not destroyed"
  );
});

add_task(clear_state);