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

/**
 * Tests the DownloadHistory module.
 */

"use strict";

const { DownloadHistory } = ChromeUtils.importESModule(
  "resource://gre/modules/DownloadHistory.sys.mjs"
);

let baseDate = new Date("2000-01-01");

/**
 * Non-fatal assertion used to test whether the downloads in the list already
 * match the expected state.
 */
function areEqual(a, b) {
  if (a === b) {
    Assert.equal(a, b);
    return true;
  }
  info(a + " !== " + b);
  return false;
}

/**
 * This allows waiting for an expected list at various points during the test.
 */
class TestView {
  constructor(expected) {
    this.expected = [...expected];
    this.downloads = [];
    this.resolveWhenExpected = () => {};
  }
  onDownloadAdded(download, options = {}) {
    if (options.insertBefore) {
      let index = this.downloads.indexOf(options.insertBefore);
      this.downloads.splice(index, 0, download);
    } else {
      this.downloads.push(download);
    }
    this.checkForExpectedDownloads();
  }
  onDownloadChanged(download) {
    this.checkForExpectedDownloads();
  }
  onDownloadRemoved(download) {
    let index = this.downloads.indexOf(download);
    this.downloads.splice(index, 1);
    this.checkForExpectedDownloads();
  }
  checkForExpectedDownloads() {
    // Wait for all the expected downloads to be added or removed before doing
    // the detailed tests. This is done to avoid creating irrelevant output.
    if (this.downloads.length != this.expected.length) {
      return;
    }
    for (let i = 0; i < this.downloads.length; i++) {
      if (
        this.downloads[i].source.url != this.expected[i].source.url ||
        this.downloads[i].target.path != this.expected[i].target.path
      ) {
        return;
      }
    }
    // Check and report the actual state of the downloads. Even if the items
    // are in the expected order, the metadata for history downloads might not
    // have been updated to the final state yet.
    for (let i = 0; i < this.downloads.length; i++) {
      let download = this.downloads[i];
      let testDownload = this.expected[i];
      info(
        "Checking download source " +
          download.source.url +
          " with target " +
          download.target.path
      );
      if (
        !areEqual(download.succeeded, !!testDownload.succeeded) ||
        !areEqual(download.canceled, !!testDownload.canceled) ||
        !areEqual(download.hasPartialData, !!testDownload.hasPartialData) ||
        !areEqual(!!download.error, !!testDownload.error)
      ) {
        return;
      }
      // If the above properties match, the error details should be correct.
      if (download.error) {
        if (testDownload.error.becauseSourceFailed) {
          Assert.equal(download.error.message, "History download failed.");
        }
        Assert.equal(
          download.error.becauseBlockedByParentalControls,
          testDownload.error.becauseBlockedByParentalControls
        );
        Assert.equal(
          download.error.becauseBlockedByReputationCheck,
          testDownload.error.becauseBlockedByReputationCheck
        );
      }
    }
    this.resolveWhenExpected();
  }
  async waitForExpected() {
    let promise = new Promise(resolve => (this.resolveWhenExpected = resolve));
    this.checkForExpectedDownloads();
    await promise;
  }
}

/**
 * Tests that various operations on session and history downloads are reflected
 * by the DownloadHistoryList object, and that the order of results is correct.
 */
add_task(async function test_DownloadHistory() {
  // Clean up at the beginning and at the end of the test.
  async function cleanup() {
    await PlacesUtils.history.clear();
  }
  registerCleanupFunction(cleanup);
  await cleanup();

  let testDownloads = [
    // History downloads should appear in order at the beginning of the list.
    { offset: 10, canceled: true },
    { offset: 20, succeeded: true },
    { offset: 30, error: { becauseSourceFailed: true } },
    { offset: 40, error: { becauseBlockedByParentalControls: true } },
    { offset: 50, error: { becauseBlockedByReputationCheck: true } },
    // Session downloads should show up after all the history download, in the
    // same order as they were added.
    { offset: 45, canceled: true, inSession: true },
    { offset: 35, canceled: true, hasPartialData: true, inSession: true },
    { offset: 55, succeeded: true, inSession: true },
  ];
  const NEXT_OFFSET = 60;

  let publicList = await promiseNewList();
  let allList = await Downloads.getList(Downloads.ALL);

  async function addTestDownload(properties) {
    properties.source = {
      url: httpUrl("source" + properties.offset),
      isPrivate: properties.isPrivate,
    };
    let targetFile = getTempFile(TEST_TARGET_FILE_NAME + properties.offset);
    properties.target = { path: targetFile.path };
    properties.startTime = new Date(baseDate.getTime() + properties.offset);

    let download = await Downloads.createDownload(properties);
    if (properties.inSession) {
      await allList.add(download);
    }

    if (properties.isPrivate) {
      return;
    }

    // Add the download to history using the XPCOM service, then use the
    // DownloadHistory module to save the associated metadata.
    let promiseFileAnnotation = waitForAnnotation(
      properties.source.url,
      "downloads/destinationFileURI"
    );
    let promiseMetaAnnotation = waitForAnnotation(
      properties.source.url,
      "downloads/metaData"
    );
    let promiseVisit = promiseWaitForVisit(properties.source.url);
    await DownloadHistory.addDownloadToHistory(download);
    await promiseVisit;
    await DownloadHistory.updateMetaData(download);
    await Promise.all([promiseFileAnnotation, promiseMetaAnnotation]);
  }

  // Add all the test downloads to history.
  for (let properties of testDownloads) {
    await addTestDownload(properties);
  }

  // Initialize DownloadHistoryList only after having added the history and
  // session downloads, and check that they are loaded in the correct order.
  let historyList = await DownloadHistory.getList();
  let view = new TestView(testDownloads);
  await historyList.addView(view);
  await view.waitForExpected();

  // Remove a download from history and verify that the change is reflected.
  let downloadToRemove = view.expected[1];
  view.expected.splice(1, 1);
  await PlacesUtils.history.remove(downloadToRemove.source.url);
  await view.waitForExpected();

  // Add a download to history and verify it's placed before session downloads,
  // even if the start date is more recent.
  let downloadToAdd = { offset: NEXT_OFFSET, canceled: true };
  view.expected.splice(
    view.expected.findIndex(d => d.inSession),
    0,
    downloadToAdd
  );
  await addTestDownload(downloadToAdd);
  await view.waitForExpected();

  // Add a session download and verify it's placed after all session downloads,
  // even if the start date is less recent.
  let sessionDownloadToAdd = { offset: 0, inSession: true, succeeded: true };
  view.expected.push(sessionDownloadToAdd);
  await addTestDownload(sessionDownloadToAdd);
  await view.waitForExpected();

  // Add a session download for the same URI without a history entry, and verify
  // it's visible and placed after all session downloads.
  view.expected.push(sessionDownloadToAdd);
  await publicList.add(await Downloads.createDownload(sessionDownloadToAdd));
  await view.waitForExpected();

  // Create a new DownloadHistoryList that also shows private downloads. Since
  // we only have public downloads, the two lists should contain the same items.
  let allHistoryList = await DownloadHistory.getList({ type: Downloads.ALL });
  let allView = new TestView(view.expected);
  await allHistoryList.addView(allView);
  await allView.waitForExpected();

  // Add a new private download and verify it appears only on the complete list.
  let privateDownloadToAdd = {
    offset: NEXT_OFFSET + 10,
    inSession: true,
    succeeded: true,
    isPrivate: true,
  };
  allView.expected.push(privateDownloadToAdd);
  await addTestDownload(privateDownloadToAdd);
  await view.waitForExpected();
  await allView.waitForExpected();

  // Now test the maxHistoryResults parameter.
  let allHistoryList2 = await DownloadHistory.getList({
    type: Downloads.ALL,
    maxHistoryResults: 3,
  });
  // Prepare the set of downloads to contain fewer history downloads by removing
  // the oldest ones.
  let allView2 = new TestView(allView.expected.slice(3));
  await allHistoryList2.addView(allView2);
  await allView2.waitForExpected();

  // Create a dummy list and view like the previous limited one to just add and
  // remove its view to make sure it doesn't break other lists' updates.
  let dummyList = await DownloadHistory.getList({
    type: Downloads.ALL,
    maxHistoryResults: 3,
  });
  let dummyView = new TestView([]);
  await dummyList.addView(dummyView);
  await dummyList.removeView(dummyView);

  // Clear history and check that session downloads with partial data remain.
  // Private downloads are also not cleared when clearing history.
  view.expected = view.expected.filter(d => d.hasPartialData);
  allView.expected = allView.expected.filter(
    d => d.hasPartialData || d.isPrivate
  );
  await PlacesUtils.history.clear();
  await view.waitForExpected();
  await allView.waitForExpected();

  // Check that the dummy view above did not prevent the limited from updating.
  allView2.expected = allView.expected;
  await allView2.waitForExpected();
});