github.com/derat/nup@v0.0.0-20230418113745-15592ba7c620/test/web/unit/updater.test.js (about)

     1  // Copyright 2021 Daniel Erat.
     2  // All rights reserved.
     3  
     4  import { afterEach, beforeEach, expectEq, suite, test } from './test.js';
     5  import MockWindow from './mock-window.js';
     6  import Updater from './updater.js';
     7  
     8  suite('updater', () => {
     9    let w = null;
    10    let updater = null;
    11  
    12    const t1 = new Date('2021-04-05T23:12:05.234Z');
    13    const t2 = new Date('2021-04-06T12:05:03Z');
    14    const t3 = new Date('2021-04-07T13:45:12.5Z');
    15  
    16    beforeEach(() => {
    17      w = new MockWindow();
    18    });
    19    afterEach(() => {
    20      updater?.destroy();
    21      updater = null;
    22      w.finish();
    23    });
    24  
    25    function initUpdater() {
    26      updater?.destroy();
    27      updater = new Updater();
    28    }
    29    function playedUrl(songId, startTime) {
    30      return (
    31        `played?songId=${encodeURIComponent(songId)}` +
    32        `&startTime=${encodeURIComponent(startTime.toISOString())}`
    33      );
    34    }
    35    function rateAndTagUrl(songId, rating, tags) {
    36      let url = `rate_and_tag?songId=${songId}`;
    37      if (rating != null) url += `&rating=${rating}`;
    38      if (tags != null) url += `&tags=${encodeURIComponent(tags.join(' '))}`;
    39      return url;
    40    }
    41  
    42    test('reportPlay (success)', async () => {
    43      initUpdater();
    44      w.expectFetch(playedUrl('123', t1), 'POST', 'ok');
    45      await updater.reportPlay('123', t1);
    46      expectEq(w.numTimeouts, 0, 'numTimeouts');
    47    });
    48  
    49    test('reportPlay (retry)', async () => {
    50      initUpdater();
    51  
    52      // Report a play and have the server return a 500 error.
    53      const id1 = '123';
    54      w.expectFetch(playedUrl(id1, t1), 'POST', 'whoops', 500);
    55      await updater.reportPlay(id1, t1);
    56  
    57      // Report a second play and have it also fail.
    58      const id2 = '456';
    59      w.expectFetch(playedUrl(id2, t2), 'POST', 'whoops', 500);
    60      await updater.reportPlay(id2, t2);
    61  
    62      // 200 ms later, nothing more should have happened.
    63      await w.runTimeouts(200);
    64  
    65      // We initially retry at 500 ms, so after 300 ms more, we should try to
    66      // report both plays again.
    67      w.expectFetch(playedUrl(id1, t1), 'POST', 'ok');
    68      w.expectFetch(playedUrl(id2, t2), 'POST', 'ok');
    69      await w.runTimeouts(300);
    70      expectEq(w.numTimeouts, 0, 'numTimeouts');
    71    });
    72  
    73    test('reportPlay (backoff)', async () => {
    74      // Make the initial attempt fail.
    75      initUpdater();
    76      w.expectFetch(playedUrl('1', t1), 'POST', 'whoops', 500);
    77      await updater.reportPlay('1', t1);
    78  
    79      // The retry time should double up to 5 minutes.
    80      for (const ms of [
    81        500, 1_000, 2_000, 4_000, 8_000, 16_000, 32_000, 64_000, 128_000, 256_000,
    82        300_000, 300_000, 300_000,
    83      ]) {
    84        w.expectFetch(playedUrl('1', t1), 'POST', 'fail', 500);
    85        await w.runTimeouts(ms);
    86      }
    87  
    88      // Try to report a second play and check that it doesn't reset the delay.
    89      w.expectFetch(playedUrl('1', t2), 'POST', 'fail', 500);
    90      await updater.reportPlay('1', t2);
    91      expectEq(w.numUnsatisfiedFetches, 0, 'Unsatisfied fetches');
    92      await w.runTimeouts(299_000);
    93  
    94      // Wait the final second and let the next attempt succeed.
    95      w.expectFetch(playedUrl('1', t1), 'POST', 'ok');
    96      w.expectFetch(playedUrl('1', t2), 'POST', 'ok');
    97      await w.runTimeouts(1_000);
    98  
    99      // Report another play and check that it's sent immediately.
   100      w.expectFetch(playedUrl('1', t3), 'POST', 'ok');
   101      await updater.reportPlay('1', t3);
   102      expectEq(w.numTimeouts, 0, 'numTimeouts');
   103    });
   104  
   105    test('reportPlay (retry at startup)', async () => {
   106      // Make a playback report fail.
   107      const id = '1';
   108      initUpdater();
   109      w.expectFetch(playedUrl(id, t1), 'POST', 'fail', 500);
   110      await updater.reportPlay(id, t1);
   111  
   112      // Report a second playback, but leave the fetch() hanging.
   113      // This should leave the playback in the "active" list in localStorage.
   114      w.expectFetchDeferred(playedUrl(id, t2), 'POST', 'fail', 500);
   115      updater.reportPlay(id, t2);
   116  
   117      // Clear timeouts to make sure the old updater isn't doing anything and
   118      // create a new updater. It should pick up both old reports from
   119      // localStorage and try to send the first one again, which again fails (and
   120      // gets moved to the end of the queue this time).
   121      w.clearTimeouts();
   122      w.expectFetch(playedUrl(id, t1), 'POST', 'fail', 500);
   123      initUpdater();
   124      await updater.initialSendDoneForTest;
   125  
   126      // After creating another updater, it should send the second report first
   127      // and then the first one.
   128      w.clearTimeouts();
   129      w.expectFetch(playedUrl(id, t2), 'POST', 'ok');
   130      initUpdater();
   131      await updater.initialSendDoneForTest;
   132  
   133      w.expectFetch(playedUrl(id, t1), 'POST', 'ok');
   134      await w.runTimeouts(0);
   135      expectEq(w.numTimeouts, 0, 'numTimeouts');
   136  
   137      // If we create a new updater again, nothing should be sent.
   138      initUpdater();
   139      await updater.initialSendDoneForTest;
   140      expectEq(w.numTimeouts, 0, 'numTimeouts');
   141    });
   142  
   143    test('reportPlay (overlapping)', async () => {
   144      // Report a play, but leave the fetch() call hanging.
   145      const id = '1';
   146      initUpdater();
   147      const finishFetch = w.expectFetchDeferred(playedUrl(id, t1), 'POST', 'ok');
   148      const reportDone = updater.reportPlay(id, t1);
   149  
   150      // Successfully report a second play in the meantime.
   151      w.expectFetch(playedUrl(id, t2), 'POST', 'ok');
   152      await updater.reportPlay(id, t2);
   153  
   154      // Let the first fetch() finish.
   155      finishFetch();
   156      await reportDone;
   157      expectEq(w.numTimeouts, 0, 'numTimeouts');
   158  
   159      // If we create a new updater, nothing should be sent.
   160      initUpdater();
   161      await updater.initialSendDoneForTest;
   162      expectEq(w.numTimeouts, 0, 'numTimeouts');
   163    });
   164  
   165    test('reportPlay (online/offline)', async () => {
   166      // Make the initial attempt file.
   167      const id = '1';
   168      initUpdater();
   169      w.expectFetch(playedUrl(id, t1), 'POST', 'fail', 500);
   170      await updater.reportPlay(id, t1);
   171  
   172      // If we're offline when the retry happens, another retry shouldn't be
   173      // scheduled.
   174      w.online = false;
   175      w.expectFetch(playedUrl(id, t1), 'POST', 'fail', 500);
   176      await w.runTimeouts(500);
   177      expectEq(w.numTimeouts, 0, 'numTimeouts');
   178  
   179      // As soon as we come back online, an immediate retry should be scheduled.
   180      w.expectFetch(playedUrl(id, t1), 'POST');
   181      w.online = true;
   182      await w.runTimeouts(0);
   183    });
   184  
   185    test('rateAndTag (success)', async () => {
   186      initUpdater();
   187      w.expectFetch(rateAndTagUrl('123', 4, null), 'POST', 'ok');
   188      await updater.rateAndTag('123', 4, null);
   189      w.expectFetch(rateAndTagUrl('123', null, ['abc', 'def']), 'POST', 'ok');
   190      await updater.rateAndTag('123', null, ['abc', 'def']);
   191      w.expectFetch(rateAndTagUrl('123', 5, ['ijk']), 'POST', 'ok');
   192      await updater.rateAndTag('123', 5, ['ijk']);
   193      expectEq(w.numTimeouts, 0, 'numTimeouts');
   194    });
   195  
   196    test('rateAndTag (retry)', async () => {
   197      initUpdater();
   198  
   199      // Rate and tag a song and have the server report failure.
   200      w.expectFetch(rateAndTagUrl('123', 2, ['old']), 'POST', 'bad', 500);
   201      await updater.rateAndTag('123', 2, ['old']);
   202  
   203      // Try to send an updated rating and tag for the same song.
   204      w.expectFetch(rateAndTagUrl('123', 4, ['new']), 'POST', 'bad', 500);
   205      await updater.rateAndTag('123', 4, ['new']);
   206  
   207      // Send a rating and tag for another song.
   208      w.expectFetch(rateAndTagUrl('456', 5, ['other']), 'POST', 'bad', 500);
   209      await updater.rateAndTag('456', 5, ['other']);
   210  
   211      // After a 500 ms delay, the latest data for each song should be sent.
   212      w.expectFetch(rateAndTagUrl('123', 4, ['new']), 'POST', 'ok');
   213      w.expectFetch(rateAndTagUrl('456', 5, ['other']), 'POST', 'ok');
   214      await w.runTimeouts(500);
   215      expectEq(w.numTimeouts, 0, 'numTimeouts');
   216    });
   217  
   218    test('rateAndTag (retry at startup)', async () => {
   219      // Make the initial attempt fail.
   220      initUpdater();
   221      w.expectFetch(rateAndTagUrl('123', 5, ['tag']), 'POST', 'bad', 500);
   222      await updater.rateAndTag('123', 5, ['tag']);
   223  
   224      // Send a second request, but leave the fetch() hanging.
   225      // This should leave the update in the "active" list in localStorage.
   226      w.expectFetchDeferred(rateAndTagUrl('456', 1, ['a']), 'POST', 'fail', 500);
   227      updater.rateAndTag('456', 1, ['a']);
   228  
   229      // Fail again with a new updater.
   230      w.clearTimeouts();
   231      w.expectFetch(rateAndTagUrl('123', 5, ['tag']), 'POST', 'bad', 500);
   232      initUpdater();
   233      await updater.initialSendDoneForTest;
   234  
   235      // Create another updater and let both updates get sent successfully.
   236      w.clearTimeouts();
   237      w.expectFetch(rateAndTagUrl('123', 5, ['tag']), 'POST', 'ok');
   238      w.expectFetch(rateAndTagUrl('456', 1, ['a']), 'POST', 'ok');
   239      initUpdater();
   240      await updater.initialSendDoneForTest;
   241      await w.runTimeouts(0);
   242      expectEq(w.numTimeouts, 0, 'numTimeouts');
   243  
   244      // If we create a new updater again, nothing should be sent.
   245      initUpdater();
   246      await updater.initialSendDoneForTest;
   247      expectEq(w.numTimeouts, 0, 'numTimeouts');
   248    });
   249  });