• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1const t = require('tap')
2const { basename } = require('path')
3const tmock = require('../../fixtures/tmock')
4const mockNpm = require('../../fixtures/mock-npm')
5
6const CURRENT_VERSION = '123.420.69'
7const CURRENT_MAJOR = '122.420.69'
8const CURRENT_MINOR = '123.419.69'
9const CURRENT_PATCH = '123.420.68'
10const NEXT_VERSION = '123.421.70'
11const NEXT_MINOR = '123.420.70'
12const NEXT_PATCH = '123.421.69'
13const CURRENT_BETA = '124.0.0-beta.99999'
14const HAVE_BETA = '124.0.0-beta.0'
15
16const runUpdateNotifier = async (t, {
17  STAT_ERROR,
18  WRITE_ERROR,
19  PACOTE_ERROR,
20  STAT_MTIME = 0,
21  mocks: _mocks = {},
22  command = 'help',
23  prefixDir,
24  version = CURRENT_VERSION,
25  argv = [],
26  wroteFile = false,
27  ...config
28} = {}) => {
29  const mockFs = {
30    ...require('fs/promises'),
31    stat: async (path) => {
32      if (basename(path) !== '_update-notifier-last-checked') {
33        t.fail('no stat allowed for non upate notifier files')
34      }
35      if (STAT_ERROR) {
36        throw STAT_ERROR
37      }
38      return { mtime: new Date(STAT_MTIME) }
39    },
40    writeFile: async (path, content) => {
41      wroteFile = true
42      if (content !== '') {
43        t.fail('no write file content allowed')
44      }
45      if (basename(path) !== '_update-notifier-last-checked') {
46        t.fail('no writefile allowed for non upate notifier files')
47      }
48      if (WRITE_ERROR) {
49        throw WRITE_ERROR
50      }
51    },
52  }
53
54  const MANIFEST_REQUEST = []
55  const mockPacote = {
56    manifest: async (spec) => {
57      if (!spec.match(/^npm@/)) {
58        t.fail('no pacote manifest allowed for non npm packages')
59      }
60      MANIFEST_REQUEST.push(spec)
61      if (PACOTE_ERROR) {
62        throw PACOTE_ERROR
63      }
64      const manifestV = spec === 'npm@latest' ? CURRENT_VERSION
65        : /-/.test(spec) ? CURRENT_BETA : NEXT_VERSION
66      return { version: manifestV }
67    },
68  }
69
70  const mocks = {
71    pacote: mockPacote,
72    'fs/promises': mockFs,
73    '{ROOT}/package.json': { version },
74    'ci-info': { isCI: false, name: null },
75    ..._mocks,
76  }
77
78  const mock = await mockNpm(t, {
79    command,
80    mocks,
81    config,
82    exec: true,
83    prefixDir,
84    argv,
85  })
86  const updateNotifier = tmock(t, '{LIB}/utils/update-notifier.js', mocks)
87
88  const result = await updateNotifier(mock.npm)
89
90  return {
91    wroteFile,
92    result,
93    MANIFEST_REQUEST,
94  }
95}
96
97t.test('duration has elapsed, no updates', async t => {
98  const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t)
99  t.equal(wroteFile, true)
100  t.not(result)
101  t.equal(MANIFEST_REQUEST.length, 1)
102})
103
104t.test('situations in which we do not notify', t => {
105  t.test('nothing to do if notifier disabled', async t => {
106    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, {
107      'update-notifier': false,
108    })
109    t.equal(wroteFile, false)
110    t.equal(result, null)
111    t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
112  })
113
114  t.test('do not suggest update if already updating', async t => {
115    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, {
116      command: 'install',
117      prefixDir: { 'package.json': `{"name":"${t.testName}"}` },
118      argv: ['npm'],
119      global: true,
120    })
121    t.equal(wroteFile, false)
122    t.equal(result, null)
123    t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
124  })
125
126  t.test('do not suggest update if already updating with spec', async t => {
127    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, {
128      command: 'install',
129      prefixDir: { 'package.json': `{"name":"${t.testName}"}` },
130      argv: ['npm@latest'],
131      global: true,
132    })
133    t.equal(wroteFile, false)
134    t.equal(result, null)
135    t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
136  })
137
138  t.test('do not update if same as latest', async t => {
139    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t)
140    t.equal(wroteFile, true)
141    t.equal(result, null)
142    t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
143  })
144  t.test('check if stat errors (here for coverage)', async t => {
145    const STAT_ERROR = new Error('blorg')
146    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_ERROR })
147    t.equal(wroteFile, true)
148    t.equal(result, null)
149    t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
150  })
151  t.test('ok if write errors (here for coverage)', async t => {
152    const WRITE_ERROR = new Error('grolb')
153    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { WRITE_ERROR })
154    t.equal(wroteFile, true)
155    t.equal(result, null)
156    t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
157  })
158  t.test('ignore pacote failures (here for coverage)', async t => {
159    const PACOTE_ERROR = new Error('pah-KO-tchay')
160    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { PACOTE_ERROR })
161    t.equal(result, null)
162    t.equal(wroteFile, true)
163    t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version')
164  })
165  t.test('do not update if newer than latest, but same as next', async t => {
166    const {
167      wroteFile,
168      result,
169      MANIFEST_REQUEST,
170    } = await runUpdateNotifier(t, { version: NEXT_VERSION })
171    t.equal(result, null)
172    t.equal(wroteFile, true)
173    const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`]
174    t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions')
175  })
176  t.test('do not update if on the latest beta', async t => {
177    const {
178      wroteFile,
179      result,
180      MANIFEST_REQUEST,
181    } = await runUpdateNotifier(t, { version: CURRENT_BETA })
182    t.equal(result, null)
183    t.equal(wroteFile, true)
184    const reqs = [`npm@^${CURRENT_BETA}`]
185    t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions')
186  })
187
188  t.test('do not update in CI', async t => {
189    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { mocks: {
190      'ci-info': { isCI: true, name: 'something' },
191    } })
192    t.equal(wroteFile, false)
193    t.equal(result, null)
194    t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
195  })
196
197  t.test('only check weekly for GA releases', async t => {
198    // One week (plus five minutes to account for test environment fuzziness)
199    const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5
200    const { wroteFile, result, MANIFEST_REQUEST } = await runUpdateNotifier(t, { STAT_MTIME })
201    t.equal(wroteFile, false, 'duration was not reset')
202    t.equal(result, null)
203    t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
204  })
205
206  t.test('only check daily for betas', async t => {
207    // One day (plus five minutes to account for test environment fuzziness)
208    const STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 5
209    const {
210      wroteFile,
211      result,
212      MANIFEST_REQUEST,
213    } = await runUpdateNotifier(t, { STAT_MTIME, version: HAVE_BETA })
214    t.equal(wroteFile, false, 'duration was not reset')
215    t.equal(result, null)
216    t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests')
217  })
218
219  t.end()
220})
221
222t.test('notification situations', async t => {
223  const cases = {
224    [HAVE_BETA]: [`^{V}`],
225    [NEXT_PATCH]: [`latest`, `^{V}`],
226    [NEXT_MINOR]: [`latest`, `^{V}`],
227    [CURRENT_PATCH]: ['latest'],
228    [CURRENT_MINOR]: ['latest'],
229    [CURRENT_MAJOR]: ['latest'],
230  }
231
232  for (const [version, reqs] of Object.entries(cases)) {
233    for (const color of [false, 'always']) {
234      await t.test(`${version} - color=${color}`, async t => {
235        const {
236          wroteFile,
237          result,
238          MANIFEST_REQUEST,
239        } = await runUpdateNotifier(t, { version, color })
240        t.matchSnapshot(result)
241        t.equal(wroteFile, true)
242        t.strictSame(MANIFEST_REQUEST, reqs.map(r => `npm@${r.replace('{V}', version)}`))
243      })
244    }
245  }
246})
247