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